killbill-memoizeit

Merge remote-tracking branch 'upstream/work-for-release-0.17.x'

9/20/2016 3:55:37 PM

Changes

account/pom.xml 2(+1 -1)

api/pom.xml 2(+1 -1)

beatrix/pom.xml 2(+1 -1)

catalog/pom.xml 2(+1 -1)

currency/pom.xml 2(+1 -1)

invoice/pom.xml 2(+1 -1)

jaxrs/pom.xml 2(+1 -1)

junction/pom.xml 2(+1 -1)

NEWS 9(+9 -0)

overdue/pom.xml 2(+1 -1)

payment/pom.xml 2(+1 -1)

pom.xml 4(+2 -2)

profiles/pom.xml 2(+1 -1)

tenant/pom.xml 2(+1 -1)

usage/pom.xml 2(+1 -1)

util/pom.xml 2(+1 -1)

Details

account/pom.xml 2(+1 -1)

diff --git a/account/pom.xml b/account/pom.xml
index 5b17600..17a4076 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-account</artifactId>
diff --git a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
index 829e000..c9cc74a 100644
--- a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
+++ b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
@@ -157,17 +157,16 @@ public class DefaultAccountInternalApi extends DefaultAccountApiBase implements 
         return accountModelDao;
     }
 
-    private int getBCDInternal(final UUID accountId, final InternalTenantContext context) {
-        final Integer bcd = accountDao.getAccountBCD(accountId, context);
-        return bcd != null ? bcd : DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL;
-    }
-
     private CacheLoaderArgument createBCDCacheLoaderArgument(final InternalTenantContext context) {
         final AccountBCDCacheLoader.LoaderCallback loaderCallback = new AccountBCDCacheLoader.LoaderCallback() {
             @Override
             public Object loadAccountBCD(final UUID accountId, final InternalTenantContext context) {
-                int bcd = getBCDInternal(accountId, context);
-                return new Integer(bcd);
+                Object result = accountDao.getAccountBCD(accountId, context);
+                if (result != null) {
+                    // If the value is 0, then account BCD was not set so we don't want to create a cache entry
+                    result = result.equals(DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL) ? null : result;
+                }
+                return result;
             }
         };
         final Object[] args = new Object[1];

api/pom.xml 2(+1 -1)

diff --git a/api/pom.xml b/api/pom.xml
index 9d3539e..63f28a3 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-internal-api</artifactId>
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 234f9f3..fada42e 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -89,7 +89,7 @@ public interface SubscriptionBaseInternalApi {
 
     public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(SubscriptionBase subscription, InternalTenantContext context);
 
-    public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, InternalTenantContext context) throws SubscriptionBaseApiException;
+    public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, List<PlanPhasePriceOverride> overrides, InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException;
 
     public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(UUID subscriptionId, @Nullable String baseProductName,
                                                                      DateTime requestedDate, InternalTenantContext context) throws SubscriptionBaseApiException;

beatrix/pom.xml 2(+1 -1)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index da2ef43..3b272b8 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-beatrix</artifactId>
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
index beb7246..7b6b8da 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueIntegration.java
@@ -46,13 +46,16 @@ import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.invoice.api.InvoicePayment;
 import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
+import org.killbill.billing.overdue.config.DefaultOverdueConfig;
 import org.killbill.billing.overdue.wrapper.OverdueWrapper;
 import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.xmlloader.XMLLoader;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.io.Resources;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
@@ -949,6 +952,151 @@ public class TestOverdueIntegration extends TestOverdueBase {
         checkODState(OverdueWrapper.CLEAR_STATE_NAME);
     }
 
+    @Test(groups = "slow", description = "Test overdue state with number of unpaid invoices condition")
+    public void testOverdueStateWithNumberOfUnpaidInvoicesCondition() throws Exception {
+        // 2012-05-01T00:03:42.000Z
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        final DefaultOverdueConfig config = XMLLoader.getObjectFromString(Resources.getResource("overdueWithNumberOfUnpaidInvoicesCondition.xml").toExternalForm(), DefaultOverdueConfig.class);
+        overdueConfigCache.loadDefaultOverdueConfig(config);
+
+        setupAccount();
+
+        paymentPlugin.makeAllInvoicesFailWithError(true);
+
+        createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE,
+                                                   term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+        // 2012-05-31 => DAY 30 have to get out of trial before first payment
+        addMonthsAndCheckForCompletion(1, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+        // Verify that number of unpaid invoices is 1
+        Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 1);
+        // Should still be in clear state
+        checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+
+        // Add 1 month
+        addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+        // Verify that number of unpaid invoices is 2
+        Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 2);
+        // Now we should be in OD1
+        checkODState("OD1");
+
+        // Add 1 month
+        addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+        // Verify that number of unpaid invoices is 3
+        Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 3);
+        // Now we should be in OD2
+        checkODState("OD2");
+
+        // Add 1 month
+        addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+        // Verify that number of unpaid invoices is 4
+        Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 4);
+        // We should still be in OD2
+        checkODState("OD2");
+
+        // Add 1 month
+        addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.TAG,  NextEvent.BLOCK);
+        // Verify that number of unpaid invoices is 5
+        Assert.assertEquals(invoiceUserApi.getUnpaidInvoicesByAccountId(account.getId(), clock.getUTCToday(), callContext).size(), 5);
+        // Now we should be in OD3
+        checkODState("OD3");
+
+        // Get all unpaid invoices and pay them to clear the overdue state
+        paymentPlugin.makeAllInvoicesFailWithError(false);
+        List<Invoice> unpaidInvoices = getUnpaidInvoicesOrderFromRecent();
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(0), NextEvent.BLOCK, NextEvent.TAG, NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(1), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(2), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(3), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(4), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+        // We should be clear now
+        checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+    }
+
+    @Test(groups = "slow", description = "Test overdue state with total unpaid invoice balance condition")
+    public void testOverdueStateWithTotalUnpaidInvoiceBalanceCondition() throws Exception {
+        // 2012-05-01T00:03:42.000Z
+        clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
+
+        final DefaultOverdueConfig config = XMLLoader.getObjectFromString(Resources.getResource("overdueWithTotalUnpaidInvoiceBalanceCondition.xml").toExternalForm(), DefaultOverdueConfig.class);
+        overdueConfigCache.loadDefaultOverdueConfig(config);
+
+        setupAccount();
+
+        paymentPlugin.makeAllInvoicesFailWithError(true);
+
+        createBaseEntitlementAndCheckForCompletion(account.getId(), "externalKey", productName, ProductCategory.BASE,
+                                                   term, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+        // 2012-05-31 => DAY 30 have to get out of trial before first payment
+        addMonthsAndCheckForCompletion(1, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+
+        // Amount balance should be USD 249.95
+        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(249.95)), 0);
+        // Should still be in clear state
+        checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+
+        // Add 1 month
+        addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+        // Amount balance should be USD 499.90
+        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(499.90)), 0);
+        // Now we should be in OD1
+        checkODState("OD1");
+
+        // Add 1 month
+        addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.BLOCK, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+        // Amount balance should be USD 749.85
+        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(749.85)), 0);
+        // Now we should be in OD2
+        checkODState("OD2");
+
+        // Add 1 month
+        addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+        // Amount balance should be USD 999.80
+        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(999.80)), 0);
+        // We should still be in OD2
+        checkODState("OD2");
+
+        // Add 1 month
+        addMonthsAndCheckForCompletion(1, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR,
+                                       NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR, NextEvent.TAG,  NextEvent.BLOCK);
+        // Amount balance should be USD 1249.75
+        assertEquals(invoiceUserApi.getAccountBalance(account.getId(), callContext).compareTo(BigDecimal.valueOf(1249.75)), 0);
+        // Now we should be in OD3
+        checkODState("OD3");
+
+        // Get all unpaid invoices and pay them to clear the overdue state
+        paymentPlugin.makeAllInvoicesFailWithError(false);
+        List<Invoice> unpaidInvoices = getUnpaidInvoicesOrderFromRecent();
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(0), NextEvent.BLOCK, NextEvent.TAG, NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(1), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(2), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(3), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+        createPaymentAndCheckForCompletion(account, unpaidInvoices.get(4), NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+        // We should be clear now
+        checkODState(OverdueWrapper.CLEAR_STATE_NAME);
+    }
+
     private void allowPaymentsAndResetOverdueToClearByPayingAllUnpaidInvoices(final boolean extraPayment) {
 
         // Reset plugin so payments should now succeed
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
index 7e1b7b3..4635782 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java
@@ -49,6 +49,7 @@ import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
 import org.killbill.billing.entitlement.api.Entitlement;
 import org.killbill.billing.entitlement.api.EntitlementApiException;
 import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.payment.api.PaymentMethodPlugin;
 import org.killbill.billing.payment.api.PluginProperty;
@@ -223,7 +224,54 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
         invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, testCallContext);
         assertEquals(invoices.size(), 3);
         assertEquals(invoices.get(2).getChargedAmount().compareTo(new BigDecimal("9.00")), 0); // 10 (recurring) - 1 (repair)
+    }
+
+
+
+    // Use custom plan definition to create a THIRTY_DAYS plan with no trial and test issue #598
+    @Test(groups = "slow")
+    public void testWithThirtyDaysPlan() throws Exception {
+
+        // Create a per-tenant catalog with one plan
+        final SimplePlanDescriptor desc1 = new DefaultSimplePlanDescriptor("thirty-monthly", "Thirty", ProductCategory.BASE, account.getCurrency(), BigDecimal.TEN, BillingPeriod.THIRTY_DAYS, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+        catalogUserApi.addSimplePlan(desc1, init, testCallContext);
+        StaticCatalog catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+        assertEquals(catalog.getCurrentPlans().length, 1);
+
+
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("thirty-monthly", null);
 
+        createEntitlement(spec, null, true);
+
+        List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, testCallContext);
+        assertEquals(invoices.size(), 1);
+        assertEquals(invoices.get(0).getChargedAmount().compareTo(BigDecimal.TEN), 0);
+        assertEquals(invoices.get(0).getInvoiceItems().size(), 1);
+
+        final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
+        expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2016, 6, 1), new LocalDate(2016, 7, 1), InvoiceItemType.RECURRING, BigDecimal.TEN));
+        invoiceChecker.checkInvoiceNoAudits(invoices.get(0), callContext, expectedInvoices);
+
+        int invoiceSize = 2;
+        LocalDate startDate = new LocalDate(2016, 7, 1);
+        for (int i = 0; i < 14; i++) {
+
+            expectedInvoices.clear();
+
+            busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+            clock.addDays(30);
+            assertListenerStatus();
+
+            LocalDate endDate = startDate.plusDays(30);
+            invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, testCallContext);
+            assertEquals(invoices.size(), invoiceSize);
+
+            expectedInvoices.add(new ExpectedInvoiceItemCheck(startDate, endDate, InvoiceItemType.RECURRING, BigDecimal.TEN));
+            invoiceChecker.checkInvoiceNoAudits(invoices.get(invoices.size() - 1), callContext, expectedInvoices);
+
+            startDate = endDate;
+            invoiceSize++;
+        }
     }
 
     private Entitlement createEntitlement(final String planName, final boolean expectPayment) throws EntitlementApiException {
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
index 695e480..17d84b4 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
@@ -248,6 +248,110 @@ public class TestSubscription extends TestIntegrationBase {
     }
 
     @Test(groups = "slow")
+    public void testCreateSubscriptionWithAddOnsWithLimitException() throws Exception {
+        final LocalDate initialDate = new LocalDate(2015, 10, 1);
+        clock.setDay(initialDate);
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec3 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        final String externalKey = "baseExternalKey";
+        EntitlementSpecifier baseEntitlementSpecifier = new DefaultEntitlementSpecifier(baseSpec, null);
+        EntitlementSpecifier addOnEntitlementSpecifier1 = new DefaultEntitlementSpecifier(addOnSpec1, null);
+        EntitlementSpecifier addOnEntitlementSpecifier2 = new DefaultEntitlementSpecifier(addOnSpec2, null);
+        EntitlementSpecifier addOnEntitlementSpecifier3 = new DefaultEntitlementSpecifier(addOnSpec3, null);
+
+        final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+        specifierList.add(baseEntitlementSpecifier);
+        specifierList.add(addOnEntitlementSpecifier1);
+        specifierList.add(addOnEntitlementSpecifier2);
+        specifierList.add(addOnEntitlementSpecifier3);
+
+        // Trying to add the third add_on with the same plan should throw an exception (the limit is 2 for this plan)
+        try {
+            entitlementApi.createBaseEntitlementWithAddOns(account.getId(), externalKey, specifierList, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        } catch (final EntitlementApiException e) {
+            assertEquals(e.getCode(), ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE.getCode());
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testCreateBaseSubscriptionAndAddOnsWithLimitException() throws Exception {
+        final LocalDate initialDate = new LocalDate(2015, 10, 1);
+        clock.setDay(initialDate);
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec3 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        // Create base subscription
+        final Entitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), account.getExternalKey(), "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        assertNotNull(baseEntitlement);
+
+        // Create first add_on subscription
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec1, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        // Create second add_on subscription with the same plan
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec2, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        // Trying to add the third add_on with the same plan should throw an exception (the limit is 2 for this plan)
+        try {
+            entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec3, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        } catch (final EntitlementApiException e) {
+            assertEquals(e.getCode(), ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE.getCode());
+        }
+    }
+
+    @Test(groups = "slow")
+    public void testChangePlanWithLimitException() throws Exception {
+        final LocalDate initialDate = new LocalDate(2015, 10, 1);
+        clock.setDay(initialDate);
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec3 = new PlanPhaseSpecifier("Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        // Create base subscription
+        final Entitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), account.getExternalKey(), "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        assertNotNull(baseEntitlement);
+
+        // Create first add_on subscription
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec1, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        // Create second add_on subscription with the same plan
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec2, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        // Create third add_on subscription with another plan
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+        Entitlement addOn3 = entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec3, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        // Trying to change the plan of the third add_on to 'Laser-Scope' plan, should throw an exception (the limit is 2 for this plan)
+        try {
+            final PlanPhaseSpecifier addOnSpecChangedPlan = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+            addOn3.changePlan(addOnSpecChangedPlan, null, ImmutableList.<PluginProperty>of(), callContext);
+        } catch (final EntitlementApiException e) {
+            assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE.getCode());
+        }
+    }
+
+    @Test(groups = "slow")
     public void testCancelFutureSubscriptionWithPolicy() throws Exception {
         final LocalDate initialDate = new LocalDate(2015, 9, 1);
         clock.setDay(initialDate);
diff --git a/beatrix/src/test/resources/overdueWithNumberOfUnpaidInvoicesCondition.xml b/beatrix/src/test/resources/overdueWithNumberOfUnpaidInvoicesCondition.xml
new file mode 100644
index 0000000..798eea1
--- /dev/null
+++ b/beatrix/src/test/resources/overdueWithNumberOfUnpaidInvoicesCondition.xml
@@ -0,0 +1,53 @@
+<!--
+  ~ Copyright 2016 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.
+  -->
+
+<overdueConfig>
+   <accountOverdueStates>
+       <state name="OD3">
+           <condition>
+               <numberOfUnpaidInvoicesEqualsOrExceeds>5</numberOfUnpaidInvoicesEqualsOrExceeds>
+           </condition>
+           <externalMessage>Reached OD3</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+       <state name="OD2">
+           <condition>
+               <numberOfUnpaidInvoicesEqualsOrExceeds>3</numberOfUnpaidInvoicesEqualsOrExceeds>
+           </condition>
+           <externalMessage>Reached OD2</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+       <state name="OD1">
+           <condition>
+               <numberOfUnpaidInvoicesEqualsOrExceeds>2</numberOfUnpaidInvoicesEqualsOrExceeds>
+           </condition>
+           <externalMessage>Reached OD1</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+   </accountOverdueStates>
+</overdueConfig>
diff --git a/beatrix/src/test/resources/overdueWithTotalUnpaidInvoiceBalanceCondition.xml b/beatrix/src/test/resources/overdueWithTotalUnpaidInvoiceBalanceCondition.xml
new file mode 100644
index 0000000..b50d999
--- /dev/null
+++ b/beatrix/src/test/resources/overdueWithTotalUnpaidInvoiceBalanceCondition.xml
@@ -0,0 +1,53 @@
+<!--
+  ~ Copyright 2016 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.
+  -->
+
+<overdueConfig>
+   <accountOverdueStates>
+       <state name="OD3">
+           <condition>
+               <totalUnpaidInvoiceBalanceEqualsOrExceeds>1200.0</totalUnpaidInvoiceBalanceEqualsOrExceeds>
+           </condition>
+           <externalMessage>Reached OD3</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+       <state name="OD2">
+           <condition>
+               <totalUnpaidInvoiceBalanceEqualsOrExceeds>600.0</totalUnpaidInvoiceBalanceEqualsOrExceeds>
+           </condition>
+           <externalMessage>Reached OD2</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+       <state name="OD1">
+           <condition>
+               <totalUnpaidInvoiceBalanceEqualsOrExceeds>260.0</totalUnpaidInvoiceBalanceEqualsOrExceeds>
+           </condition>
+           <externalMessage>Reached OD1</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+   </accountOverdueStates>
+</overdueConfig>

catalog/pom.xml 2(+1 -1)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index dd6f964..1f72d70 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-catalog</artifactId>
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
index 5e94a51..3b54252 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
@@ -75,6 +75,11 @@ public class EhCacheOverriddenPlanCache implements OverriddenPlanCache {
         return (DefaultPlan) cacheController.get(planName, argument);
     }
 
+    @Override
+    public void addDryRunPlan(final String planName, final Plan plan) {
+        cacheController.putIfAbsent(planName, plan);
+    }
+
     private DefaultPlan loadOverriddenPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException {
 
         final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(planName);
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/OverriddenPlanCache.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/OverriddenPlanCache.java
index f0f8178..f888dc8 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/OverriddenPlanCache.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/OverriddenPlanCache.java
@@ -20,9 +20,12 @@ package org.killbill.billing.catalog.caching;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.DefaultPlan;
 import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.StaticCatalog;
 
 public interface OverriddenPlanCache {
 
     DefaultPlan getOverriddenPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException;
+
+    void addDryRunPlan(final String planName, final Plan plan);
 }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
index 684e21e..dd0dcfd 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
@@ -175,7 +175,17 @@ public class CatalogUpdater {
 
         if (desc.getProductCategory() == ProductCategory.ADD_ON) {
             for (final String bp : desc.getAvailableBaseProducts()) {
-                catalog.addProductAvailableAO(getExistingProduct(bp), product);
+                final DefaultProduct targetBasePlan = getExistingProduct(bp);
+                boolean found = false;
+                for (DefaultProduct cur : targetBasePlan.getAvailable()) {
+                    if (cur.getName().equals(product.getName())) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (!found) {
+                    catalog.addProductAvailableAO(getExistingProduct(bp), product);
+                }
             }
         }
 
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
index 885b7cb..856fd91 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -76,7 +76,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
     //No other value is allowed for Tiered ADDONS
     //A value of -1 means unlimited
     @XmlElement(required = false)
-    private Integer plansAllowedInBundle = 1;
+    private Integer plansAllowedInBundle = -1;
 
     private String priceListName;
 
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/override/DefaultPriceOverride.java b/catalog/src/main/java/org/killbill/billing/catalog/override/DefaultPriceOverride.java
index c8165b5..86b8739 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/override/DefaultPriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/override/DefaultPriceOverride.java
@@ -18,8 +18,11 @@
 package org.killbill.billing.catalog.override;
 
 import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.regex.Pattern;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTime;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.callcontext.InternalCallContext;
@@ -43,6 +46,8 @@ import com.google.inject.Inject;
 
 public class DefaultPriceOverride implements PriceOverride {
 
+    private static final AtomicLong DRY_RUN_PLAN_IDX = new AtomicLong(0);
+
     public static final Pattern CUSTOM_PLAN_NAME_PATTERN = Pattern.compile("(.*)-(\\d+)$");
 
     private final CatalogOverrideDao overrideDao;
@@ -55,7 +60,7 @@ public class DefaultPriceOverride implements PriceOverride {
     }
 
     @Override
-    public DefaultPlan getOrCreateOverriddenPlan(final Plan parentPlan, final DateTime catalogEffectiveDate, final List<PlanPhasePriceOverride> overrides, final InternalCallContext context) throws CatalogApiException {
+    public DefaultPlan getOrCreateOverriddenPlan(final Plan parentPlan, final DateTime catalogEffectiveDate, final List<PlanPhasePriceOverride> overrides, @Nullable final InternalCallContext context) throws CatalogApiException {
 
         final PlanPhasePriceOverride[] resolvedOverride = new PlanPhasePriceOverride[parentPlan.getAllPhases().length];
         int index = 0;
@@ -97,9 +102,17 @@ public class DefaultPriceOverride implements PriceOverride {
             }
         }
 
-        final CatalogOverridePlanDefinitionModelDao overriddenPlan = overrideDao.getOrCreateOverridePlanDefinition(parentPlan.getName(), catalogEffectiveDate, resolvedOverride, context);
-        final String planName = new StringBuffer(parentPlan.getName()).append("-").append(overriddenPlan.getRecordId()).toString();
+        final String planName;
+        if (context != null) {
+            final CatalogOverridePlanDefinitionModelDao overriddenPlan = overrideDao.getOrCreateOverridePlanDefinition(parentPlan.getName(), catalogEffectiveDate, resolvedOverride, context);
+            planName = new StringBuffer(parentPlan.getName()).append("-").append(overriddenPlan.getRecordId()).toString();
+        } else {
+            planName = new StringBuffer(parentPlan.getName()).append("-dryrun-").append(DRY_RUN_PLAN_IDX.incrementAndGet()).toString();
+        }
         final DefaultPlan result = new DefaultPlan(planName, (DefaultPlan) parentPlan, resolvedOverride);
+        if (context == null) {
+            overriddenPlanCache.addDryRunPlan(planName, result);
+        }
         return result;
     }
 
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
index 8a458c4..fbffc1f 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
@@ -78,7 +78,7 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
             return defaultPlan;
         }
 
-        final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContextWithoutAccountRecordId(overrides.getCallContext());
+        final InternalCallContext internalCallContext = overrides.getCallContext() != null ? internalCallContextFactory.createInternalCallContextWithoutAccountRecordId(overrides.getCallContext()) : null;
         return priceOverride.getOrCreateOverriddenPlan(defaultPlan, CatalogDateHelper.toUTCDateTime(getEffectiveDate()), overrides.getOverrides(), internalCallContext);
     }
 
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
index c101cd0..6f2af55 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
@@ -383,7 +383,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
                                    "                </recurring>\n" +
                                    "                <usages/>\n" +
                                    "            </finalPhase>\n" +
-                                   "            <plansAllowedInBundle>1</plansAllowedInBundle>\n" +
+                                   "            <plansAllowedInBundle>-1</plansAllowedInBundle>\n" +
                                    "        </plan>\n" +
                                    "        <plan name=\"sports-monthly\">\n" +
                                    "            <product>Sports</product>\n" +
@@ -428,7 +428,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
                                    "                </recurring>\n" +
                                    "                <usages/>\n" +
                                    "            </finalPhase>\n" +
-                                   "            <plansAllowedInBundle>1</plansAllowedInBundle>\n" +
+                                   "            <plansAllowedInBundle>-1</plansAllowedInBundle>\n" +
                                    "        </plan>\n" +
                                    "        <plan name=\"super-monthly\">\n" +
                                    "            <product>Super</product>\n" +
@@ -473,7 +473,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
                                    "                </recurring>\n" +
                                    "                <usages/>\n" +
                                    "            </finalPhase>\n" +
-                                   "            <plansAllowedInBundle>1</plansAllowedInBundle>\n" +
+                                   "            <plansAllowedInBundle>-1</plansAllowedInBundle>\n" +
                                    "        </plan>\n" +
                                    "        <plan name=\"dynamic-annual\">\n" +
                                    "            <product>Dynamic</product>\n" +
@@ -510,7 +510,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
                                    "                </recurring>\n" +
                                    "                <usages/>\n" +
                                    "            </finalPhase>\n" +
-                                   "            <plansAllowedInBundle>1</plansAllowedInBundle>\n" +
+                                   "            <plansAllowedInBundle>-1</plansAllowedInBundle>\n" +
                                    "        </plan>\n" +
                                    "    </plans>\n" +
                                    "    <priceLists>\n" +
diff --git a/catalog/src/test/resources/catalogTest.xml b/catalog/src/test/resources/catalogTest.xml
index a1cdec3..670abb5 100644
--- a/catalog/src/test/resources/catalogTest.xml
+++ b/catalog/src/test/resources/catalogTest.xml
@@ -838,6 +838,7 @@
                     </recurringPrice>
                 </recurring>
             </finalPhase>
+            <plansAllowedInBundle>2</plansAllowedInBundle>
         </plan>
         <plan name="cleaning-monthly">
             <product>Cleaning</product>

currency/pom.xml 2(+1 -1)

diff --git a/currency/pom.xml b/currency/pom.xml
index 149fd67..c39bb22 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-currency</artifactId>
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 8fe8984..992df8e 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-entitlement</artifactId>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index 3e5fa05..1f84737 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -35,6 +35,7 @@ import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanPhase;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
@@ -559,9 +560,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
                 final DateTime effectiveChangeDate;
                 try {
-                    effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, null, context);
+                    effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, null, overrides, context);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+                } catch (final CatalogApiException e) {
+                    throw new EntitlementApiException(e, e.getCode(), e.getMessage());
                 }
 
                 try {
@@ -624,9 +627,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
                 final DateTime resultingEffectiveDate;
                 try {
-                    resultingEffectiveDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, effectiveChangeDate, null, context);
+                    resultingEffectiveDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, effectiveChangeDate, null, overrides, context);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+                } catch (final CatalogApiException e) {
+                    throw new EntitlementApiException(e, e.getCode(), e.getMessage());
                 }
 
                 try {
@@ -689,9 +694,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
 
                 final DateTime effectiveChangeDate;
                 try {
-                    effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, actionPolicy, context);
+                    effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, actionPolicy, overrides, context);
                 } catch (final SubscriptionBaseApiException e) {
                     throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+                } catch (final CatalogApiException e) {
+                    throw new EntitlementApiException(e, e.getCode(), e.getMessage());
                 }
 
                 try {

invoice/pom.xml 2(+1 -1)

diff --git a/invoice/pom.xml b/invoice/pom.xml
index f1795d3..68088ab 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-invoice</artifactId>
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 65f339f..79a0a45 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,14 +17,13 @@
 
 package org.killbill.billing.invoice.config;
 
-import java.lang.reflect.Method;
-
 import javax.inject.Inject;
 import javax.inject.Named;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.glue.InvoiceModule;
 import org.killbill.billing.util.config.definition.InvoiceConfig;
+import org.killbill.billing.util.config.definition.KillbillConfig;
 import org.killbill.billing.util.config.tenant.CacheConfig;
 import org.killbill.billing.util.config.tenant.MultiTenantConfigBase;
 import org.skife.config.TimeSpan;
@@ -46,8 +45,7 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
 
     @Override
     public int getNumberOfMonthsInFuture(final InternalTenantContext tenantContext) {
-        final Method method = new Object(){}.getClass().getEnclosingMethod();
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getNumberOfMonthsInFuture", tenantContext);
         if (result != null) {
             return Integer.parseInt(result);
         }
@@ -61,8 +59,7 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
 
     @Override
     public TimeSpan getDryRunNotificationSchedule(final InternalTenantContext tenantContext) {
-        final Method method = new Object(){}.getClass().getEnclosingMethod();
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getDryRunNotificationSchedule", tenantContext);
         if (result != null) {
             return new TimeSpan(result);
         }
@@ -76,8 +73,7 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
 
     @Override
     public int getMaxRawUsagePreviousPeriod(final InternalTenantContext tenantContext) {
-        final Method method = new Object(){}.getClass().getEnclosingMethod();
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getMaxRawUsagePreviousPeriod", tenantContext);
         if (result != null) {
             return Integer.parseInt(result);
         }
@@ -95,11 +91,7 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
     }
 
     @Override
-    protected Method getConfigStaticMethod(final String methodName) {
-        try {
-            return InvoiceConfig.class.getMethod(methodName, InternalTenantContext.class);
-        } catch (final NoSuchMethodException e) {
-            throw new RuntimeException(e);
-        }
+    protected Class<? extends KillbillConfig> getConfigClass() {
+        return InvoiceConfig.class;
     }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
index 846b057..8a75ef0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/BillingIntervalDetail.java
@@ -32,12 +32,14 @@ public class BillingIntervalDetail {
     private final int billingCycleDay;
     private final BillingPeriod billingPeriod;
     private final BillingMode billingMode;
+    private final boolean isMonthBased;
     // First date after the startDate aligned with the BCD
     private LocalDate firstBillingCycleDate;
     // Date up to which we should bill
     private LocalDate effectiveEndDate;
     private LocalDate lastBillingCycleDate;
 
+
     public BillingIntervalDetail(final LocalDate startDate,
                                  final LocalDate endDate,
                                  final LocalDate targetDate,
@@ -54,6 +56,7 @@ public class BillingIntervalDetail {
         }
         this.billingPeriod = billingPeriod;
         this.billingMode = billingMode;
+        this.isMonthBased = (billingPeriod.getPeriod().getMonths() | billingPeriod.getPeriod().getYears()) > 0;
         computeAll();
     }
 
@@ -67,7 +70,7 @@ public class BillingIntervalDetail {
 
     public LocalDate getFutureBillingDateFor(final int nbPeriod) {
         final LocalDate proposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, nbPeriod);
-        return alignProposedBillCycleDate(proposedDate, billingCycleDay);
+        return alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
     }
 
     public LocalDate getLastBillingCycleDate() {
@@ -76,7 +79,7 @@ public class BillingIntervalDetail {
 
     public LocalDate getNextBillingCycleDate() {
         final LocalDate proposedDate = lastBillingCycleDate != null ? lastBillingCycleDate.plus(billingPeriod.getPeriod()) : firstBillingCycleDate;
-        final LocalDate nextBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+        final LocalDate nextBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
         return nextBillingCycleDate;
     }
 
@@ -106,7 +109,7 @@ public class BillingIntervalDetail {
         while (proposedDate.isBefore(startDate)) {
             proposedDate = proposedDate.plus(billingPeriod.getPeriod());
         }
-        firstBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+        firstBillingCycleDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
     }
 
     private void calculateEffectiveEndDate() {
@@ -138,7 +141,7 @@ public class BillingIntervalDetail {
             nextProposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, numberOfPeriods);
             numberOfPeriods += 1;
         }
-        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
 
         // We honor the endDate as long as it does not go beyond our targetDate (by construction this cannot be after the nextProposedDate neither.
         if (endDate != null && !endDate.isAfter(targetDate)) {
@@ -168,7 +171,7 @@ public class BillingIntervalDetail {
             proposedDate = InvoiceDateUtils.advanceByNPeriods(firstBillingCycleDate, billingPeriod, numberOfPeriods);
             numberOfPeriods += 1;
         }
-        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
 
         // The proposedDate is greater to our endDate => return it
         if (endDate != null && endDate.isBefore(proposedDate)) {
@@ -196,7 +199,7 @@ public class BillingIntervalDetail {
 
         // Our proposed date is billingCycleDate prior to the effectiveEndDate
         proposedDate = proposedDate.minus(billingPeriod.getPeriod());
-        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay);
+        proposedDate = alignProposedBillCycleDate(proposedDate, billingCycleDay, isMonthBased);
 
         if (proposedDate.isBefore(firstBillingCycleDate)) {
             // Make sure not to go too far in the past
@@ -209,7 +212,11 @@ public class BillingIntervalDetail {
     //
     // We start from a billCycleDate
     //
-    public static LocalDate alignProposedBillCycleDate(final LocalDate proposedDate, final int billingCycleDay) {
+    private static LocalDate alignProposedBillCycleDate(final LocalDate proposedDate, final int billingCycleDay, final boolean isMonthBased) {
+        // billingCycleDay alignment only makes sense for month based BillingPeriod (MONTHLY, QUARTERLY, BIANNUAL, ANNUAL)
+        if (!isMonthBased) {
+            return proposedDate;
+        }
         final int lastDayOfMonth = proposedDate.dayOfMonth().getMaximumValue();
         int proposedBillCycleDate = proposedDate.getDayOfMonth();
         if (proposedBillCycleDate < billingCycleDay && billingCycleDay <= lastDayOfMonth) {
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20160908172551__multiple_refunds_254.sql b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20160908172551__multiple_refunds_254.sql
new file mode 100644
index 0000000..e5628d6
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20160908172551__multiple_refunds_254.sql
@@ -0,0 +1 @@
+drop index idx_invoice_payments on invoice_payments;
\ No newline at end of file

jaxrs/pom.xml 2(+1 -1)

diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index d477452..5e2bb13 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-jaxrs</artifactId>
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java
index 112f24b..79a2b8b 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java
@@ -947,7 +947,7 @@ public class CatalogJson {
             this.number = number;
         }
 
-        public DurationJson(final Duration duration) throws CurrencyValueNull {
+        public DurationJson(final Duration duration) {
             this(duration.getUnit(), duration.getNumber());
         }
 
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
new file mode 100644
index 0000000..048a3c5
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 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.jaxrs.json;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.jaxrs.json.CatalogJson.DurationJson;
+import org.killbill.billing.overdue.api.OverdueCondition;
+import org.killbill.billing.overdue.config.DefaultDuration;
+import org.killbill.billing.overdue.config.DefaultOverdueCondition;
+import org.killbill.billing.payment.api.PaymentResponse;
+import org.killbill.billing.util.tag.ControlTagType;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class OverdueConditionJson {
+
+    private final DurationJson timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+    private final ControlTagType controlTagInclusion;
+    private final ControlTagType controlTagExclusion;
+    private final Integer numberOfUnpaidInvoicesEqualsOrExceeds;
+    private final PaymentResponse[] responseForLastFailedPayment;
+    private final BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds;
+
+
+    @JsonCreator
+    public OverdueConditionJson(@JsonProperty("timeSinceEarliestUnpaidInvoiceEqualsOrExceeds") final DurationJson timeSinceEarliestUnpaidInvoiceEqualsOrExceeds,
+                                @JsonProperty("controlTagInclusion") final ControlTagType controlTagInclusion,
+                                @JsonProperty("controlTagExclusion") final ControlTagType controlTagExclusion,
+                                @JsonProperty("numberOfUnpaidInvoicesEqualsOrExceeds") final Integer numberOfUnpaidInvoicesEqualsOrExceeds,
+                                @JsonProperty("responseForLastFailedPayment") final PaymentResponse[] responseForLastFailedPayment,
+                                @JsonProperty("totalUnpaidInvoiceBalanceEqualsOrExceeds") final BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds) {
+        this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+        this.controlTagInclusion = controlTagInclusion;
+        this.controlTagExclusion = controlTagExclusion;
+        this.numberOfUnpaidInvoicesEqualsOrExceeds = numberOfUnpaidInvoicesEqualsOrExceeds;
+        this.responseForLastFailedPayment = responseForLastFailedPayment;
+        this.totalUnpaidInvoiceBalanceEqualsOrExceeds = totalUnpaidInvoiceBalanceEqualsOrExceeds;
+    }
+
+    public OverdueConditionJson(final OverdueCondition overdueCondition) {
+        this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = new DurationJson(overdueCondition.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds());
+        this.controlTagInclusion = overdueCondition.getInclusionControlTagType();
+        this.controlTagExclusion = overdueCondition.getExclusionControlTagType();
+        this.numberOfUnpaidInvoicesEqualsOrExceeds = overdueCondition.getNumberOfUnpaidInvoicesEqualsOrExceeds();
+        this.responseForLastFailedPayment = overdueCondition.getResponseForLastFailedPaymentIn();
+        this.totalUnpaidInvoiceBalanceEqualsOrExceeds = overdueCondition.getTotalUnpaidInvoiceBalanceEqualsOrExceeds();
+    }
+
+    public DurationJson getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds() {
+        return timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+    }
+
+    public ControlTagType getControlTagInclusion() {
+        return controlTagInclusion;
+    }
+
+    public ControlTagType getControlTagExclusion() {
+        return controlTagExclusion;
+    }
+
+    public Integer getNumberOfUnpaidInvoicesEqualsOrExceeds() {
+        return numberOfUnpaidInvoicesEqualsOrExceeds;
+    }
+
+    public PaymentResponse[] getResponseForLastFailedPayment() {
+        return responseForLastFailedPayment;
+    }
+
+    public BigDecimal getTotalUnpaidInvoiceBalanceEqualsOrExceeds() {
+        return totalUnpaidInvoiceBalanceEqualsOrExceeds;
+    }
+
+    @Override
+    public String toString() {
+        return "OverdueConditionJson{" +
+               "timeSinceEarliestUnpaidInvoiceEqualsOrExceeds=" + timeSinceEarliestUnpaidInvoiceEqualsOrExceeds +
+               ", controlTagInclusion=" + controlTagInclusion +
+               ", controlTagExclusion=" + controlTagExclusion +
+               ", numberOfUnpaidInvoicesEqualsOrExceeds=" + numberOfUnpaidInvoicesEqualsOrExceeds +
+               ", responseForLastFailedPayment=" + responseForLastFailedPayment +
+               ", totalUnpaidInvoiceBalanceEqualsOrExceeds=" + totalUnpaidInvoiceBalanceEqualsOrExceeds +
+               '}';
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof OverdueConditionJson)) {
+            return false;
+        }
+
+        final OverdueConditionJson that = (OverdueConditionJson) o;
+
+        if (timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null ? !timeSinceEarliestUnpaidInvoiceEqualsOrExceeds.equals(that.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds) : that.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null) {
+            return false;
+        }
+        if (controlTagInclusion != that.controlTagInclusion) {
+            return false;
+        }
+        if (controlTagExclusion != that.controlTagExclusion) {
+            return false;
+        }
+        if (numberOfUnpaidInvoicesEqualsOrExceeds != that.numberOfUnpaidInvoicesEqualsOrExceeds) {
+            return false;
+        }
+        if (responseForLastFailedPayment != that.responseForLastFailedPayment) {
+            return false;
+        }
+        return totalUnpaidInvoiceBalanceEqualsOrExceeds == that.totalUnpaidInvoiceBalanceEqualsOrExceeds;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds != null ? timeSinceEarliestUnpaidInvoiceEqualsOrExceeds.hashCode() : 0;
+        result = 31 * result + (controlTagInclusion != null ? controlTagInclusion.hashCode() : 0);
+        result = 31 * result + (controlTagExclusion != null ? controlTagExclusion.hashCode() : 0);
+        result = 31 * result + (numberOfUnpaidInvoicesEqualsOrExceeds != null ? numberOfUnpaidInvoicesEqualsOrExceeds.hashCode() : 0);
+        result = 31 * result + (responseForLastFailedPayment != null ? responseForLastFailedPayment.hashCode() : 0);
+        result = 31 * result + (totalUnpaidInvoiceBalanceEqualsOrExceeds != null ? totalUnpaidInvoiceBalanceEqualsOrExceeds.hashCode() : 0);
+        return result;
+    }
+
+    public static DefaultOverdueCondition toOverdueCondition(final OverdueConditionJson input) {
+        final DefaultOverdueCondition result = new DefaultOverdueCondition();
+        if (input.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds() != null) {
+            result.setTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds(new DefaultDuration().setUnit(input.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getUnit()).setNumber(input.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getNumber()));
+        }
+        result.setControlTagInclusion(input.getControlTagInclusion());
+        result.setControlTagExclusion(input.getControlTagExclusion());
+        result.setNumberOfUnpaidInvoicesEqualsOrExceeds(input.getNumberOfUnpaidInvoicesEqualsOrExceeds());
+        result.setResponseForLastFailedPayment(input.getResponseForLastFailedPayment());
+        result.setTotalUnpaidInvoiceBalanceEqualsOrExceeds(input.getTotalUnpaidInvoiceBalanceEqualsOrExceeds());
+
+        return result;
+    }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueJson.java
new file mode 100644
index 0000000..c2284be
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueJson.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 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.jaxrs.json;
+
+import java.util.List;
+
+import org.killbill.billing.catalog.api.CurrencyValueNull;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.overdue.api.OverdueApiException;
+import org.killbill.billing.overdue.api.OverdueConfig;
+import org.killbill.billing.overdue.api.OverdueState;
+import org.killbill.billing.overdue.config.DefaultDuration;
+import org.killbill.billing.overdue.config.DefaultOverdueConfig;
+import org.killbill.billing.overdue.config.DefaultOverdueState;
+import org.killbill.billing.overdue.config.DefaultOverdueStatesAccount;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class OverdueJson {
+
+    private final Integer initialReevaluationIntervalDays;
+    private final List<OverdueStateConfigJson> overdueStates;
+
+    @JsonCreator
+    public OverdueJson(@JsonProperty("initialReevaluationInterval") final Integer initialReevaluationInterval,
+                       @JsonProperty("overdueStates") final List<OverdueStateConfigJson> overdueStates) {
+        this.initialReevaluationIntervalDays = initialReevaluationInterval;
+        this.overdueStates = overdueStates;
+    }
+
+    public OverdueJson(final OverdueConfig overdueConfig) {
+        this.initialReevaluationIntervalDays = overdueConfig.getOverdueStatesAccount().getInitialReevaluationInterval() != null ?
+                                               overdueConfig.getOverdueStatesAccount().getInitialReevaluationInterval().getDays() : null;
+        this.overdueStates = ImmutableList.copyOf(Iterables.transform(ImmutableList.copyOf(overdueConfig.getOverdueStatesAccount().getStates()), new Function<OverdueState, OverdueStateConfigJson>() {
+            @Override
+            public OverdueStateConfigJson apply(final OverdueState input) {
+                    return new OverdueStateConfigJson(input);
+            }
+        }));
+    }
+
+    public Integer getInitialReevaluationInterval() {
+        return initialReevaluationIntervalDays;
+    }
+
+    public List<OverdueStateConfigJson> getOverdueStates() {
+        return overdueStates;
+    }
+
+    @Override
+    public String toString() {
+        return "OverdueJson{" +
+               "initialReevaluationIntervalDays=" + initialReevaluationIntervalDays +
+               ", overdueStates=" + overdueStates +
+               '}';
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof OverdueJson)) {
+            return false;
+        }
+
+        final OverdueJson that = (OverdueJson) o;
+
+        if (initialReevaluationIntervalDays != null ? !initialReevaluationIntervalDays.equals(that.initialReevaluationIntervalDays) : that.initialReevaluationIntervalDays != null) {
+            return false;
+        }
+        return overdueStates != null ? overdueStates.equals(that.overdueStates) : that.overdueStates == null;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = initialReevaluationIntervalDays != null ? initialReevaluationIntervalDays.hashCode() : 0;
+        result = 31 * result + (overdueStates != null ? overdueStates.hashCode() : 0);
+        return result;
+    }
+
+    public static OverdueConfig toOverdueConfigWithValidation(final OverdueJson input) {
+        final DefaultOverdueConfig result = new DefaultOverdueConfig();
+        final DefaultOverdueStatesAccount overdueStateAccount = new DefaultOverdueStatesAccount();
+        result.setOverdueStates(overdueStateAccount);
+
+        final DefaultOverdueState [] states = new DefaultOverdueState[input.getOverdueStates().size()];
+        int i = 0;
+
+
+        int prevTimeSinceEarliestUnpaidInvoice = -1;
+        for (final OverdueStateConfigJson cur : input.getOverdueStates()) {
+
+            Preconditions.checkNotNull(cur.getName());
+
+            // We only support timeSinceEarliestUnpaidInvoiceEqualsOrExceeds condition (see #611)
+            Preconditions.checkNotNull(cur.getCondition());
+            Preconditions.checkNotNull(cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds());
+            Preconditions.checkNotNull(cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getUnit());
+            Preconditions.checkState(cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getUnit() == TimeUnit.DAYS);
+            Preconditions.checkState(cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getNumber() > 0);
+
+            final DefaultOverdueState state = new DefaultOverdueState();
+            state.setName(cur.getName());
+            state.setExternalMessage(cur.getExternalMessage());
+            state.setBlockChanges(cur.getBlockChanges());
+            state.setDisableEntitlement(cur.getDisableEntitlement());
+            state.setSubscriptionCancellationPolicy(cur.getSubscriptionCancellationPolicy());
+            state.setClearState(cur.isClearState());
+            state.setAutoReevaluationInterval(computeReevaluationInterval(cur.getAutoReevaluationIntervalDays(), prevTimeSinceEarliestUnpaidInvoice, cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getNumber()));
+            state.setCondition(OverdueConditionJson.toOverdueCondition(cur.getCondition()));
+            states[i++] = state;
+
+            prevTimeSinceEarliestUnpaidInvoice = cur.getCondition().getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds().getNumber();
+        }
+        overdueStateAccount.setAccountOverdueStates(states);
+        overdueStateAccount.setInitialReevaluationInterval(computeReevaluationInterval(null, prevTimeSinceEarliestUnpaidInvoice, 0));
+        return result;
+    }
+
+    // Unless the user knows what it's doing (inputReevaluationInterval != null), for time based condition we set the reevaluation interval to match the transition to the next state
+    private static DefaultDuration computeReevaluationInterval(final Integer inputReevaluationInterval,  int prevTimeSinceEarliestUnpaidInvoice, int curTimeSinceEarliestUnpaidInvoice) {
+        if (inputReevaluationInterval != null && inputReevaluationInterval > 0) {
+            return new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(inputReevaluationInterval);
+        }
+
+        if (prevTimeSinceEarliestUnpaidInvoice == -1) {
+            return null;
+        }
+
+        Preconditions.checkState(prevTimeSinceEarliestUnpaidInvoice - curTimeSinceEarliestUnpaidInvoice > 0);
+
+        return new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(prevTimeSinceEarliestUnpaidInvoice - curTimeSinceEarliestUnpaidInvoice);
+    }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateConfigJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateConfigJson.java
new file mode 100644
index 0000000..ab06260
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueStateConfigJson.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 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.jaxrs.json;
+
+import org.killbill.billing.overdue.api.OverdueApiException;
+import org.killbill.billing.overdue.api.OverdueCancellationPolicy;
+import org.killbill.billing.overdue.api.OverdueState;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class OverdueStateConfigJson {
+
+    private final String name;
+    private final Boolean isClearState;
+    private final OverdueConditionJson condition;
+    private final String externalMessage;
+    private final Boolean blockChanges;
+    private final Boolean disableEntitlement;
+    private final OverdueCancellationPolicy subscriptionCancellationPolicy;
+    private final Integer autoReevaluationIntervalDays;
+
+    @JsonCreator
+    public OverdueStateConfigJson(@JsonProperty("name") final String name,
+                                  @JsonProperty("isClearState") final Boolean isClearState,
+                                  @JsonProperty("condition") final OverdueConditionJson condition,
+                                  @JsonProperty("externalMessage") final String externalMessage,
+                                  @JsonProperty("blockChanges") final Boolean blockChanges,
+                                  @JsonProperty("disableEntitlement") final Boolean disableEntitlement,
+                                  @JsonProperty("subscriptionCancellationPolicy") final OverdueCancellationPolicy subscriptionCancellationPolicy,
+                                  @JsonProperty("autoReevaluationIntervalDays") final Integer autoReevaluationInterval) {
+        this.name = name;
+        this.isClearState = isClearState;
+        this.condition = condition;
+        this.externalMessage = externalMessage;
+        this.blockChanges = blockChanges;
+        this.disableEntitlement = disableEntitlement;
+        this.subscriptionCancellationPolicy = subscriptionCancellationPolicy;
+        this.autoReevaluationIntervalDays = autoReevaluationInterval;
+    }
+
+    public OverdueStateConfigJson(final OverdueState input) {
+        this.name = input.getName();
+        this.isClearState = input.isClearState();
+        this.condition = input.getOverdueCondition() != null ? new OverdueConditionJson(input.getOverdueCondition()) : null;
+        this.externalMessage = input.getExternalMessage();
+        this.blockChanges = input.isBlockChanges();
+        this.disableEntitlement = input.isDisableEntitlementAndChangesBlocked();
+        this.subscriptionCancellationPolicy = input.getOverdueCancellationPolicy();
+        Integer tmpAutoReevaluationIntervalDays = null;
+        try {
+            tmpAutoReevaluationIntervalDays = input.getAutoReevaluationInterval().getDays();
+        } catch (final OverdueApiException e) {
+        } finally {
+            this.autoReevaluationIntervalDays = tmpAutoReevaluationIntervalDays;
+        }
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    @JsonProperty("isClearState")
+    public Boolean isClearState() {
+        return isClearState;
+    }
+
+    public OverdueConditionJson getCondition() {
+        return condition;
+    }
+
+    public String getExternalMessage() {
+        return externalMessage;
+    }
+
+    public Boolean getBlockChanges() {
+        return blockChanges;
+    }
+
+    public Boolean getDisableEntitlement() {
+        return disableEntitlement;
+    }
+
+    public OverdueCancellationPolicy getSubscriptionCancellationPolicy() {
+        return subscriptionCancellationPolicy;
+    }
+
+    public Integer getAutoReevaluationIntervalDays() {
+        return autoReevaluationIntervalDays;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof OverdueStateConfigJson)) {
+            return false;
+        }
+
+        final OverdueStateConfigJson that = (OverdueStateConfigJson) o;
+
+        if (name != null ? !name.equals(that.name) : that.name != null) {
+            return false;
+        }
+        if (isClearState != null ? !isClearState.equals(that.isClearState) : that.isClearState != null) {
+            return false;
+        }
+        if (condition != null ? !condition.equals(that.condition) : that.condition != null) {
+            return false;
+        }
+        if (externalMessage != null ? !externalMessage.equals(that.externalMessage) : that.externalMessage != null) {
+            return false;
+        }
+        if (blockChanges != null ? !blockChanges.equals(that.blockChanges) : that.blockChanges != null) {
+            return false;
+        }
+        if (disableEntitlement != null ? !disableEntitlement.equals(that.disableEntitlement) : that.disableEntitlement != null) {
+            return false;
+        }
+        if (subscriptionCancellationPolicy != that.subscriptionCancellationPolicy) {
+            return false;
+        }
+        return autoReevaluationIntervalDays != null ? autoReevaluationIntervalDays.equals(that.autoReevaluationIntervalDays) : that.autoReevaluationIntervalDays == null;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = name != null ? name.hashCode() : 0;
+        result = 31 * result + (isClearState != null ? isClearState.hashCode() : 0);
+        result = 31 * result + (condition != null ? condition.hashCode() : 0);
+        result = 31 * result + (externalMessage != null ? externalMessage.hashCode() : 0);
+        result = 31 * result + (blockChanges != null ? blockChanges.hashCode() : 0);
+        result = 31 * result + (disableEntitlement != null ? disableEntitlement.hashCode() : 0);
+        result = 31 * result + (subscriptionCancellationPolicy != null ? subscriptionCancellationPolicy.hashCode() : 0);
+        result = 31 * result + (autoReevaluationIntervalDays != null ? autoReevaluationIntervalDays.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "OverdueStateConfigJson{" +
+               "name='" + name + '\'' +
+               ", isClearState=" + isClearState +
+               ", condition=" + condition +
+               ", externalMessage='" + externalMessage + '\'' +
+               ", blockChanges=" + blockChanges +
+               ", disableEntitlement=" + disableEntitlement +
+               ", subscriptionCancellationPolicy=" + subscriptionCancellationPolicy +
+               ", autoReevaluationIntervalDays=" + autoReevaluationIntervalDays +
+               '}';
+    }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/OverdueResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/OverdueResource.java
index a30a0cf..534d07e 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/OverdueResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/OverdueResource.java
@@ -33,9 +33,11 @@ import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
 
 import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.jaxrs.json.OverdueJson;
 import org.killbill.billing.jaxrs.util.Context;
 import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
 import org.killbill.billing.overdue.api.OverdueApi;
+import org.killbill.billing.overdue.api.OverdueConfig;
 import org.killbill.billing.overdue.config.DefaultOverdueConfig;
 import org.killbill.billing.payment.api.PaymentApi;
 import org.killbill.billing.util.api.AuditUserApi;
@@ -54,6 +56,7 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponses;
 
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 import static javax.ws.rs.core.MediaType.APPLICATION_XML;
 
 @Singleton
@@ -80,7 +83,7 @@ public class OverdueResource extends JaxRsResourceBase {
     @TimedResource
     @GET
     @Produces(APPLICATION_XML)
-    @ApiOperation(value = "Retrieve the full catalog as XML", response = String.class, hidden = true)
+    @ApiOperation(value = "Retrieve the overdue config as XML", response = String.class, hidden = true)
     @ApiResponses(value = {})
     public Response getOverdueConfigXml(@javax.ws.rs.core.Context final HttpServletRequest request) throws Exception {
         final TenantContext tenantContext = context.createContext(request);
@@ -106,4 +109,38 @@ public class OverdueResource extends JaxRsResourceBase {
         overdueApi.uploadOverdueConfig(overdueXML, callContext);
         return uriBuilder.buildResponse(uriInfo, OverdueResource.class, null, null);
     }
+
+    @TimedResource
+    @GET
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Retrieve the overdue config as JSON" , response = OverdueJson.class)
+    @ApiResponses(value = {})
+    public Response getOverdueConfigJson(@javax.ws.rs.core.Context final HttpServletRequest request) throws Exception {
+        final TenantContext tenantContext = context.createContext(request);
+        final OverdueConfig overdueConfig = overdueApi.getOverdueConfig(tenantContext);
+        final OverdueJson result = new OverdueJson(overdueConfig);
+        return Response.status(Status.OK).entity(result).build();
+    }
+
+
+
+    @TimedResource
+    @POST
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Upload the full overdue config as JSON")
+    @ApiResponses(value = {})
+    public Response uploadOverdueConfigJson(final OverdueJson overdueJson,
+                                  @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                  @HeaderParam(HDR_REASON) final String reason,
+                                  @HeaderParam(HDR_COMMENT) final String comment,
+                                  @javax.ws.rs.core.Context final HttpServletRequest request,
+                                  @javax.ws.rs.core.Context final UriInfo uriInfo) throws Exception {
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+        final OverdueConfig overdueConfig = OverdueJson.toOverdueConfigWithValidation(overdueJson);
+        overdueApi.uploadOverdueConfig(overdueConfig, callContext);
+        return uriBuilder.buildResponse(uriInfo, OverdueResource.class, null, null);
+    }
+
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
index 7a6b6aa..ee09c1a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/JaxrsUriBuilder.java
@@ -17,6 +17,7 @@
 package org.killbill.billing.jaxrs.util;
 
 import java.net.URI;
+import java.util.HashMap;
 import java.util.Map;
 
 import javax.annotation.Nullable;
@@ -33,6 +34,10 @@ import org.killbill.billing.util.config.definition.JaxrsConfig;
 public class JaxrsUriBuilder {
 
     private final JaxrsConfig jaxrsConfig;
+    private final Map<Class, UriBuilder> classToUriBuilder = new HashMap<Class, UriBuilder>();
+    private final Map<String, UriBuilder> classAndMethodToUriBuilder = new HashMap<String, UriBuilder>();
+    private final Map<String, UriBuilder> pathAndClassToUriBuilder = new HashMap<String, UriBuilder>();
+    private final Map<String, UriBuilder> pathClassAndMethodToUriBuilder = new HashMap<String, UriBuilder>();
 
     @Inject
     public JaxrsUriBuilder(JaxrsConfig jaxrsConfig) {
@@ -53,8 +58,8 @@ public class JaxrsUriBuilder {
 
         if (jaxrsConfig.isJaxrsLocationFullUrl()) {
             uriBuilder.scheme(uriInfo.getAbsolutePath().getScheme())
-              .host(uriInfo.getAbsolutePath().getHost())
-              .port(uriInfo.getAbsolutePath().getPort());
+                      .host(uriInfo.getAbsolutePath().getHost())
+                      .port(uriInfo.getAbsolutePath().getPort());
         }
         return objectId != null ? uriBuilder.build(objectId) : uriBuilder.build();
     }
@@ -92,15 +97,74 @@ public class JaxrsUriBuilder {
 
     private UriBuilder getUriBuilder(final String path, final Class<? extends JaxrsResource> theClassMaybeEnhanced, @Nullable final String getMethodName) {
         final Class theClass = getNonEnhancedClass(theClassMaybeEnhanced);
-        return getMethodName != null ? UriBuilder.fromPath(path.equals("/") ? path.substring(1) : path).path(theClass).path(theClass, getMethodName) :
-               UriBuilder.fromPath(path).path(theClass);
+        return getMethodName != null ? fromPath(path.equals("/") ? path.substring(1) : path, theClass, getMethodName) : fromPath(path, theClass);
+    }
+
+    private UriBuilder fromPath(final String path, final Class theClass, final String getMethodName) {
+        final String key = path + theClass.getName() + getMethodName;
+
+        UriBuilder uriBuilder = pathClassAndMethodToUriBuilder.get(key);
+        if (uriBuilder == null) {
+            synchronized (pathClassAndMethodToUriBuilder) {
+                uriBuilder = pathClassAndMethodToUriBuilder.get(key);
+                if (uriBuilder == null) {
+                    uriBuilder = fromPath(path, theClass).path(theClass, getMethodName);
+                    pathClassAndMethodToUriBuilder.put(key, uriBuilder);
+                }
+            }
+        }
+        return uriBuilder.clone();
+    }
+
+    private UriBuilder fromPath(final String path, final Class theClass) {
+        final String key = path + theClass.getName();
+
+        UriBuilder uriBuilder = pathAndClassToUriBuilder.get(key);
+        if (uriBuilder == null) {
+            synchronized (pathAndClassToUriBuilder) {
+                uriBuilder = pathAndClassToUriBuilder.get(key);
+                if (uriBuilder == null) {
+                    uriBuilder = UriBuilder.fromPath(path).path(theClass);
+                    pathAndClassToUriBuilder.put(key, uriBuilder);
+                }
+            }
+        }
+        return uriBuilder.clone();
     }
 
     private UriBuilder getUriBuilder(final Class<? extends JaxrsResource> theClassMaybeEnhanced, @Nullable final String getMethodName) {
         final Class theClass = getNonEnhancedClass(theClassMaybeEnhanced);
-        return getMethodName != null ? UriBuilder.fromResource(theClass).path(theClass, getMethodName) :
-               UriBuilder.fromResource(theClass);
+        return getMethodName != null ? fromResource(theClass, getMethodName) : fromResource(theClass);
+    }
 
+    private UriBuilder fromResource(final Class theClass, final String getMethodName) {
+        final String key = theClass.getName() + getMethodName;
+
+        UriBuilder uriBuilder = classAndMethodToUriBuilder.get(key);
+        if (uriBuilder == null) {
+            synchronized (classAndMethodToUriBuilder) {
+                uriBuilder = classAndMethodToUriBuilder.get(key);
+                if (uriBuilder == null) {
+                    uriBuilder = fromResource(theClass).path(theClass, getMethodName);
+                    classAndMethodToUriBuilder.put(key, uriBuilder);
+                }
+            }
+        }
+        return uriBuilder.clone();
+    }
+
+    private UriBuilder fromResource(final Class theClass) {
+        UriBuilder uriBuilder = classToUriBuilder.get(theClass);
+        if (uriBuilder == null) {
+            synchronized (classToUriBuilder) {
+                uriBuilder = classToUriBuilder.get(theClass);
+                if (uriBuilder == null) {
+                    uriBuilder = UriBuilder.fromResource(theClass);
+                    classToUriBuilder.put(theClass, uriBuilder);
+                }
+            }
+        }
+        return uriBuilder.clone();
     }
 
     private Class getNonEnhancedClass(final Class<? extends JaxrsResource> theClassMaybeEnhanced) {

junction/pom.xml 2(+1 -1)

diff --git a/junction/pom.xml b/junction/pom.xml
index 0f5af56..e18f08a 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-junction</artifactId>

NEWS 9(+9 -0)

diff --git a/NEWS b/NEWS
index a9b9b12..06725dc 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+0.17.4
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.17.4
+
+0.17.3
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.17.3
+
 0.17.2
     See https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
 
@@ -7,6 +13,9 @@
 0.17.0
     See https://github.com/killbill/killbill/releases/tag/killbill-0.17.0
 
+0.16.8
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.16.8
+
 0.16.7
     See https://github.com/killbill/killbill/releases/tag/killbill-0.16.7
 

overdue/pom.xml 2(+1 -1)

diff --git a/overdue/pom.xml b/overdue/pom.xml
index f247210..8b07ded 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-overdue</artifactId>
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueApi.java b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueApi.java
index 580e4f9..05a52c9 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueApi.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/api/DefaultOverdueApi.java
@@ -21,6 +21,7 @@ import java.util.UUID;
 
 import javax.inject.Inject;
 
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.entitlement.api.BlockingState;
 import org.killbill.billing.entitlement.api.BlockingStateType;
@@ -36,6 +37,7 @@ import org.killbill.billing.tenant.api.TenantUserApi;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.xmlloader.XMLWriter;
 
 public class DefaultOverdueApi implements OverdueApi {
 
@@ -77,6 +79,16 @@ public class DefaultOverdueApi implements OverdueApi {
     }
 
     @Override
+    public void uploadOverdueConfig(final OverdueConfig overdueConfig, final CallContext callContext) throws OverdueApiException {
+        try {
+            final String overdueXML = XMLWriter.writeXML((DefaultOverdueConfig) overdueConfig, DefaultOverdueConfig.class);
+            uploadOverdueConfig(overdueXML, callContext);
+        } catch (final Exception e) {
+            throw new OverdueApiException(ErrorCode.OVERDUE_INVALID_FOR_TENANT, callContext.getTenantId());
+        }
+    }
+
+    @Override
     public OverdueState getOverdueStateFor(final UUID accountId, final TenantContext tenantContext) throws OverdueApiException {
         final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, tenantContext);
         final BlockingState blockingStateForService = blockingInternalApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalTenantContext);
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
index cf2ea4f..2d01711 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueStateApplicator.java
@@ -288,7 +288,7 @@ public class OverdueStateApplicator {
     }
 
     private boolean blockChanges(final OverdueState nextOverdueState) {
-        return nextOverdueState.isBlockChanges();
+        return nextOverdueState.isBlockChanges() || nextOverdueState.isDisableEntitlementAndChangesBlocked();
     }
 
     private boolean blockBilling(final OverdueState nextOverdueState) {
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java
index d1d73c2..49396ea 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultDuration.java
@@ -111,12 +111,12 @@ public class DefaultDuration extends ValidatingConfig<DefaultOverdueConfig> impl
         return errors;
     }
 
-    protected DefaultDuration setUnit(final TimeUnit unit) {
+    public DefaultDuration setUnit(final TimeUnit unit) {
         this.unit = unit;
         return this;
     }
 
-    protected DefaultDuration setNumber(final Integer number) {
+    public DefaultDuration setNumber(final Integer number) {
         this.number = number;
         return this;
     }
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
index 3cca103..a4e4b51 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
@@ -157,6 +157,30 @@ public class DefaultOverdueCondition extends ValidatingConfig<DefaultOverdueConf
         return controlTagExclusion;
     }
 
+    public void setNumberOfUnpaidInvoicesEqualsOrExceeds(final Integer numberOfUnpaidInvoicesEqualsOrExceeds) {
+        this.numberOfUnpaidInvoicesEqualsOrExceeds = numberOfUnpaidInvoicesEqualsOrExceeds;
+    }
+
+    public void setTotalUnpaidInvoiceBalanceEqualsOrExceeds(final BigDecimal totalUnpaidInvoiceBalanceEqualsOrExceeds) {
+        this.totalUnpaidInvoiceBalanceEqualsOrExceeds = totalUnpaidInvoiceBalanceEqualsOrExceeds;
+    }
+
+    public void setTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds(final DefaultDuration timeSinceEarliestUnpaidInvoiceEqualsOrExceeds) {
+        this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+    }
+
+    public void setResponseForLastFailedPayment(final PaymentResponse[] responseForLastFailedPayment) {
+        this.responseForLastFailedPayment = responseForLastFailedPayment;
+    }
+
+    public void setControlTagInclusion(final ControlTagType controlTagInclusion) {
+        this.controlTagInclusion = controlTagInclusion;
+    }
+
+    public void setControlTagExclusion(final ControlTagType controlTagExclusion) {
+        this.controlTagExclusion = controlTagExclusion;
+    }
+
     @Override
     public String toString() {
         final StringBuilder sb = new StringBuilder("DefaultOverdueCondition{");
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
index 9d33ddd..1068745 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueState.java
@@ -98,7 +98,7 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig> 
 
     @Override
     public boolean isBlockChanges() {
-        return blockChanges || disableEntitlement;
+        return blockChanges;
     }
 
     @Override
@@ -119,22 +119,26 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig> 
         return autoReevaluationInterval.toJodaPeriod();
     }
 
-    protected DefaultOverdueState setName(final String name) {
+    public void setAutoReevaluationInterval(final DefaultDuration autoReevaluationInterval) {
+        this.autoReevaluationInterval = autoReevaluationInterval;
+    }
+
+    public DefaultOverdueState setName(final String name) {
         this.name = name;
         return this;
     }
 
-    protected DefaultOverdueState setClearState(final boolean isClearState) {
+    public DefaultOverdueState setClearState(final boolean isClearState) {
         this.isClearState = isClearState;
         return this;
     }
 
-    protected DefaultOverdueState setExternalMessage(final String externalMessage) {
+    public DefaultOverdueState setExternalMessage(final String externalMessage) {
         this.externalMessage = externalMessage;
         return this;
     }
 
-    protected DefaultOverdueState setDisableEntitlement(final boolean cancel) {
+    public DefaultOverdueState setDisableEntitlement(final boolean cancel) {
         this.disableEntitlement = cancel;
         return this;
     }
@@ -144,12 +148,12 @@ public class DefaultOverdueState extends ValidatingConfig<DefaultOverdueConfig> 
         return this;
     }
 
-    protected DefaultOverdueState setBlockChanges(final boolean cancel) {
+    public DefaultOverdueState setBlockChanges(final boolean cancel) {
         this.blockChanges = cancel;
         return this;
     }
 
-    protected DefaultOverdueState setCondition(final DefaultOverdueCondition condition) {
+    public DefaultOverdueState setCondition(final DefaultOverdueCondition condition) {
         this.condition = condition;
         return this;
     }
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
index 0bd4e63..004fd2d 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueStatesAccount.java
@@ -45,12 +45,12 @@ public class DefaultOverdueStatesAccount extends DefaultOverdueStateSet implemen
         return initialReevaluationInterval.toJodaPeriod();
     }
 
-    protected DefaultOverdueStatesAccount setAccountOverdueStates(final DefaultOverdueState[] accountOverdueStates) {
+    public DefaultOverdueStatesAccount setAccountOverdueStates(final DefaultOverdueState[] accountOverdueStates) {
         this.accountOverdueStates = accountOverdueStates;
         return this;
     }
 
-    protected DefaultOverdueStatesAccount setInitialReevaluationInterval(final DefaultDuration initialReevaluationInterval) {
+    public DefaultOverdueStatesAccount setInitialReevaluationInterval(final DefaultDuration initialReevaluationInterval) {
         this.initialReevaluationInterval = initialReevaluationInterval;
         return this;
     }
diff --git a/overdue/src/test/resources/OverdueConfig3.xml b/overdue/src/test/resources/OverdueConfig3.xml
new file mode 100644
index 0000000..ddba32f
--- /dev/null
+++ b/overdue/src/test/resources/OverdueConfig3.xml
@@ -0,0 +1,71 @@
+<!--
+  ~ Copyright 2014-2016 Groupon, Inc
+  ~ Copyright 2014-2016 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.
+  -->
+
+<overdueConfig>
+    <accountOverdueStates>
+        <state name="OD4">
+            <condition>
+                <numberOfUnpaidInvoicesEqualsOrExceeds>5</numberOfUnpaidInvoicesEqualsOrExceeds>
+                <controlTagInclusion>AUTO_PAY_OFF</controlTagInclusion>
+            </condition>
+            <externalMessage>Reached OD3</externalMessage>
+            <blockChanges>true</blockChanges>
+            <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+            <autoReevaluationInterval>
+                <unit>DAYS</unit><number>5</number>
+            </autoReevaluationInterval>
+        </state>
+        <state name="OD3">
+            <condition>
+                <responseForLastFailedPaymentIn>
+                    <response>INVALID_CARD</response>
+                    <response>LOST_OR_STOLEN_CARD</response>
+                </responseForLastFailedPaymentIn>
+            </condition>
+            <externalMessage>Reached OD3</externalMessage>
+            <blockChanges>true</blockChanges>
+            <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+            <autoReevaluationInterval>
+                <unit>DAYS</unit><number>5</number>
+            </autoReevaluationInterval>
+        </state>
+        <state name="OD2">
+            <condition>
+                <totalUnpaidInvoiceBalanceEqualsOrExceeds>5.00</totalUnpaidInvoiceBalanceEqualsOrExceeds>
+            </condition>
+            <externalMessage>Reached OD2</externalMessage>
+            <blockChanges>true</blockChanges>
+            <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+            <autoReevaluationInterval>
+                <unit>DAYS</unit><number>5</number>
+            </autoReevaluationInterval>
+        </state>
+        <state name="OD1">
+            <condition>
+                <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+                    <unit>DAYS</unit><number>30</number>
+                </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+            </condition>
+            <externalMessage>Reached OD1</externalMessage>
+            <blockChanges>true</blockChanges>
+            <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+            <autoReevaluationInterval>
+                <unit>DAYS</unit><number>5</number>
+            </autoReevaluationInterval>
+        </state>
+    </accountOverdueStates>
+</overdueConfig>

payment/pom.xml 2(+1 -1)

diff --git a/payment/pom.xml b/payment/pom.xml
index d47ebd3..f2d255f 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-payment</artifactId>
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
index 1d3d1b2..5e44397 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentApi.java
@@ -466,7 +466,6 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
             checkNotNullParameter(currency, "currency");
         }
         checkNotNullParameter(paymentId, "paymentId");
-        checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
         checkNotNullParameter(properties, "plugin properties");
 
         final String transactionType = TransactionType.REFUND.name();
@@ -731,7 +730,6 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
     public Payment createChargebackReversal(final Account account, final UUID paymentId, final String paymentTransactionExternalKey, final CallContext callContext) throws PaymentApiException {
         checkNotNullParameter(account, "account");
         checkNotNullParameter(paymentId, "paymentId");
-        checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
 
         final String transactionType = TransactionType.CHARGEBACK.name();
         Payment payment = null;
@@ -770,7 +768,6 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
 
         checkNotNullParameter(account, "account");
         checkNotNullParameter(paymentId, "paymentId");
-        checkNotNullParameter(paymentTransactionExternalKey, "paymentTransactionExternalKey");
 
         final String transactionType = TransactionType.CHARGEBACK.name();
         Payment payment = null;
diff --git a/payment/src/main/java/org/killbill/billing/payment/config/MultiTenantPaymentConfig.java b/payment/src/main/java/org/killbill/billing/payment/config/MultiTenantPaymentConfig.java
index bbf365f..6e56c52 100644
--- a/payment/src/main/java/org/killbill/billing/payment/config/MultiTenantPaymentConfig.java
+++ b/payment/src/main/java/org/killbill/billing/payment/config/MultiTenantPaymentConfig.java
@@ -17,7 +17,6 @@
 
 package org.killbill.billing.payment.config;
 
-import java.lang.reflect.Method;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -25,6 +24,7 @@ import javax.inject.Named;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.billing.util.config.definition.KillbillConfig;
 import org.killbill.billing.util.config.definition.PaymentConfig;
 import org.killbill.billing.util.config.tenant.CacheConfig;
 import org.killbill.billing.util.config.tenant.MultiTenantConfigBase;
@@ -48,11 +48,9 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
 
     @Override
     public List<Integer> getPaymentFailureRetryDays(@Param("dummy") final InternalTenantContext tenantContext) {
-        // There is no good way to achieve that in java; this solution is expensive (we could consider hardcoding the method name each time instead)
-        final Method method = new Object() {}.getClass().getEnclosingMethod();
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getPaymentFailureRetryDays", tenantContext);
         if (result != null) {
-            return convertToListInteger(result, method.getName());
+            return convertToListInteger(result, "getPaymentFailureRetryDays");
         }
         return getPaymentFailureRetryDays();
     }
@@ -64,8 +62,7 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
 
     @Override
     public int getPluginFailureInitialRetryInSec(@Param("dummy") final InternalTenantContext tenantContext) {
-        final Method method = new Object() {}.getClass().getEnclosingMethod();
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getPluginFailureInitialRetryInSec", tenantContext);
         if (result != null) {
             return Integer.parseInt(result);
         }
@@ -79,9 +76,7 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
 
     @Override
     public int getPluginFailureRetryMultiplier(@Param("dummy") final InternalTenantContext tenantContext) {
-        final Method method = new Object() {}.getClass().getEnclosingMethod();
-
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getPluginFailureRetryMultiplier", tenantContext);
         if (result != null) {
             return Integer.parseInt(result);
         }
@@ -95,11 +90,9 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
 
     @Override
     public List<TimeSpan> getIncompleteTransactionsRetries(@Param("dummy") final InternalTenantContext tenantContext) {
-        final Method method = new Object() {}.getClass().getEnclosingMethod();
-
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getIncompleteTransactionsRetries", tenantContext);
         if (result != null) {
-            return convertToListTimeSpan(result, method.getName());
+            return convertToListTimeSpan(result, "getIncompleteTransactionsRetries");
         }
         return getIncompleteTransactionsRetries();
     }
@@ -111,9 +104,7 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
 
     @Override
     public int getPluginFailureRetryMaxAttempts(@Param("dummy") final InternalTenantContext tenantContext) {
-        final Method method = new Object() {}.getClass().getEnclosingMethod();
-
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getPluginFailureRetryMaxAttempts", tenantContext);
         if (result != null) {
             return Integer.parseInt(result);
         }
@@ -127,11 +118,9 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
 
     @Override
     public List<String> getPaymentControlPluginNames(@Param("dummy") final InternalTenantContext tenantContext) {
-        final Method method = new Object() {}.getClass().getEnclosingMethod();
-
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getPaymentControlPluginNames", tenantContext);
         if (result != null) {
-            return convertToListString(result, method.getName());
+            return convertToListString(result, "getPaymentControlPluginNames");
         }
         return getPaymentControlPluginNames();
     }
@@ -167,11 +156,7 @@ public class MultiTenantPaymentConfig extends MultiTenantConfigBase implements P
     }
 
     @Override
-    protected Method getConfigStaticMethod(final String methodName) {
-        try {
-            return PaymentConfig.class.getMethod(methodName, InternalTenantContext.class);
-        } catch (final NoSuchMethodException e) {
-            throw new RuntimeException(e);
-        }
+    protected Class<? extends KillbillConfig> getConfigClass() {
+        return PaymentConfig.class;
     }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index e8981f9..ab61ca5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -55,8 +55,8 @@ import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.core.janitor.IncompletePaymentTransactionTask;
 import org.killbill.billing.payment.core.sm.PaymentAutomatonDAOHelper;
 import org.killbill.billing.payment.core.sm.PaymentAutomatonRunner;
-import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
 import org.killbill.billing.payment.core.sm.PaymentStateContext;
+import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
 import org.killbill.billing.payment.dao.PaymentDao;
 import org.killbill.billing.payment.dao.PaymentModelDao;
 import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
@@ -456,6 +456,11 @@ public class PaymentProcessor extends ProcessorBase {
                 throw new PaymentApiException(ErrorCode.PAYMENT_DIFFERENT_ACCOUNT_ID, paymentStateContext.getPaymentId());
             }
 
+            if (paymentStateContext.getPaymentTransactionExternalKey() != null) {
+                final List<PaymentTransactionModelDao> allPaymentTransactionsForKey = daoHelper.getPaymentDao().getPaymentTransactionsByExternalKey(paymentStateContext.getPaymentTransactionExternalKey(), internalCallContext);
+                runSanityOnTransactionExternalKey(allPaymentTransactionsForKey, paymentStateContext, internalCallContext);
+            }
+
             if (paymentStateContext.getTransactionId() != null || paymentStateContext.getPaymentTransactionExternalKey() != null) {
                 // If a transaction id or key is passed, we are maybe completing an existing transaction (unless a new key was provided)
                 final List<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment = daoHelper.getPaymentDao().getTransactionsForPayment(paymentStateContext.getPaymentId(), paymentStateContext.getInternalCallContext());
@@ -499,6 +504,31 @@ public class PaymentProcessor extends ProcessorBase {
         return getPayment(nonNullPaymentId, true, false, properties, callContext, internalCallContext);
     }
 
+    private void runSanityOnTransactionExternalKey(final Iterable<PaymentTransactionModelDao> allPaymentTransactionsForKey,
+                                                   final PaymentStateContext paymentStateContext,
+                                                   final InternalCallContext internalCallContext) throws PaymentApiException {
+        for (final PaymentTransactionModelDao paymentTransactionModelDao : allPaymentTransactionsForKey) {
+            // Sanity: verify we don't already have a successful transaction for that key (chargeback reversals are a bit special, it's the only transaction type we can revert)
+            if (paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()) &&
+                paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS &&
+                paymentTransactionModelDao.getTransactionType() != TransactionType.CHARGEBACK) {
+                throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
+            }
+
+            // Sanity: don't share keys across accounts
+            if (!paymentTransactionModelDao.getAccountRecordId().equals(internalCallContext.getAccountRecordId())) {
+                UUID accountId;
+                try {
+                    accountId = accountInternalApi.getAccountByRecordId(paymentTransactionModelDao.getAccountRecordId(), internalCallContext).getId();
+                } catch (final AccountApiException e) {
+                    log.warn("Unable to retrieve account", e);
+                    accountId = null;
+                }
+                throw new PaymentApiException(ErrorCode.PAYMENT_TRANSACTION_DIFFERENT_ACCOUNT_ID, accountId);
+            }
+        }
+    }
+
     private PaymentTransactionModelDao findTransactionToCompleteAndRunSanityChecks(final PaymentModelDao paymentModelDao,
                                                                                    final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment,
                                                                                    final PaymentStateContext paymentStateContext,
@@ -524,26 +554,6 @@ public class PaymentProcessor extends ProcessorBase {
                 throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "transactionType", String.format("%s doesn't match existing transaction type %s", paymentStateContext.getTransactionType(), paymentTransactionModelDao.getTransactionType()));
             }
 
-            // Sanity: verify we don't already have a successful transaction for that key (chargeback reversals are a bit special, it's the only transaction type we can revert)
-            if (paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()) &&
-                paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS &&
-                paymentTransactionModelDao.getTransactionType() != TransactionType.CHARGEBACK) {
-                throw new PaymentApiException(ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS, paymentStateContext.getPaymentTransactionExternalKey());
-            }
-
-            // Sanity: don't share keys across accounts
-            if (paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()) &&
-                !paymentTransactionModelDao.getAccountRecordId().equals(internalCallContext.getAccountRecordId())) {
-                UUID accountId;
-                try {
-                    accountId = accountInternalApi.getAccountByRecordId(paymentModelDao.getAccountRecordId(), internalCallContext).getId();
-                } catch (final AccountApiException e) {
-                    log.warn("Unable to retrieve account", e);
-                    accountId = null;
-                }
-                throw new PaymentApiException(ErrorCode.PAYMENT_TRANSACTION_DIFFERENT_ACCOUNT_ID, accountId);
-            }
-
             // UNKNOWN transactions are potential candidates, we'll invoke the Janitor first though
             if (paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING || paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.UNKNOWN) {
                 completionCandidates.add(paymentTransactionModelDao);
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
index 16a43e7..e141506 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApi.java
@@ -2008,7 +2008,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
             createPayment(TransactionType.CAPTURE, authorization.getId(), null, authKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
             Assert.fail();
         } catch (final PaymentApiException e) {
-            Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+            Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
         }
 
         try {
@@ -2080,7 +2080,7 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
             createPayment(TransactionType.AUTHORIZE, null, null, captureKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
             Assert.fail();
         } catch (final PaymentApiException e) {
-            Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_INVALID_PARAMETER.getCode());
+            Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
         }
 
         try {
@@ -2098,6 +2098,44 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
         Assert.assertEquals(capturedPayment2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
         Assert.assertEquals(capturedPayment2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
         Assert.assertEquals(capturedPayment2.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+        final String refundKey = UUID.randomUUID().toString();
+        final Payment refundedPayment = createPayment(TransactionType.REFUND, authorization.getId(), null, refundKey, BigDecimal.ONE, PaymentPluginStatus.PROCESSED);
+        Assert.assertEquals(refundedPayment.getTransactions().size(), 4);
+        Assert.assertEquals(refundedPayment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+        Assert.assertEquals(refundedPayment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+        Assert.assertEquals(refundedPayment.getTransactions().get(2).getTransactionStatus(), TransactionStatus.SUCCESS);
+        Assert.assertEquals(refundedPayment.getTransactions().get(3).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+        // Second payment
+
+        final String auth2Key = UUID.randomUUID().toString();
+        final Payment authorization2 = createPayment(TransactionType.AUTHORIZE, null, null, auth2Key, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+        assertNotNull(authorization2);
+        Assert.assertEquals(authorization2.getTransactions().size(), 1);
+        Assert.assertEquals(authorization2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+        try {
+            // Capture with an existing successful transaction external key should fail
+            createPayment(TransactionType.CAPTURE, authorization2.getId(), null, captureKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+            Assert.fail();
+        } catch (final PaymentApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+        }
+
+        final String capture2Key = UUID.randomUUID().toString();
+        final Payment capture2 = createPayment(TransactionType.CAPTURE, authorization2.getId(), null, capture2Key, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+        Assert.assertEquals(capture2.getTransactions().size(), 2);
+        Assert.assertEquals(capture2.getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+        Assert.assertEquals(capture2.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
+
+        try {
+            // Refund with an existing successful transaction external key should fail
+            createPayment(TransactionType.REFUND, authorization2.getId(), null, refundKey, BigDecimal.TEN, PaymentPluginStatus.PROCESSED);
+            Assert.fail();
+        } catch (final PaymentApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.PAYMENT_ACTIVE_TRANSACTION_KEY_EXISTS.getCode());
+        }
     }
 
     @Test(groups = "slow")
@@ -2301,6 +2339,14 @@ public class TestPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
                                                 paymentTransactionExternalKey,
                                                 pluginProperties,
                                                 callContext);
+            case REFUND:
+                return paymentApi.createRefund(account,
+                                               paymentId,
+                                               amount,
+                                               amount == null ? null : account.getCurrency(),
+                                               paymentTransactionExternalKey,
+                                               pluginProperties,
+                                               callContext);
             default:
                 Assert.fail();
                 return null;

pom.xml 4(+2 -2)

diff --git a/pom.xml b/pom.xml
index 8c91c40..0927ec9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,10 +21,10 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.127-SNAPSHOT</version>
+        <version>0.129-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
-    <version>0.17.3-SNAPSHOT</version>
+    <version>0.17.5-SNAPSHOT</version>
     <packaging>pom</packaging>
     <name>killbill</name>
     <description>Library for managing recurring subscriptions and the associated billing</description>
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index fc683f3..c5a14ad 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killbill</artifactId>
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/config/MultiTenantNotificationConfig.java b/profiles/killbill/src/main/java/org/killbill/billing/server/config/MultiTenantNotificationConfig.java
index cfea29b..08ffd8a 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/config/MultiTenantNotificationConfig.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/config/MultiTenantNotificationConfig.java
@@ -18,10 +18,13 @@
 package org.killbill.billing.server.config;
 
 import java.lang.reflect.Method;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.payment.glue.PaymentModule;
+import org.killbill.billing.util.config.definition.KillbillConfig;
 import org.killbill.billing.util.config.definition.NotificationConfig;
 import org.killbill.billing.util.config.tenant.CacheConfig;
 import org.killbill.billing.util.config.tenant.MultiTenantConfigBase;
@@ -33,6 +36,7 @@ import com.google.inject.name.Named;
 
 public class MultiTenantNotificationConfig extends MultiTenantConfigBase implements NotificationConfig {
 
+    private final Map<String, Method> methodsCache = new HashMap<String, Method>();
     private final NotificationConfig staticConfig;
 
     @Inject
@@ -42,27 +46,21 @@ public class MultiTenantNotificationConfig extends MultiTenantConfigBase impleme
     }
 
     @Override
-    protected Method getConfigStaticMethod(final String methodName) {
-        try {
-            return NotificationConfig.class.getMethod(methodName, InternalTenantContext.class);
-        } catch (final NoSuchMethodException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    @Override
     public List<TimeSpan> getPushNotificationsRetries() {
         return staticConfig.getPushNotificationsRetries();
     }
 
     @Override
     public List<TimeSpan> getPushNotificationsRetries(@Param("dummy") final InternalTenantContext tenantContext) {
-        final Method method = new Object() {}.getClass().getEnclosingMethod();
-
-        final String result = getStringTenantConfig(method.getName(), tenantContext);
+        final String result = getStringTenantConfig("getPushNotificationsRetries", tenantContext);
         if (result != null) {
-            return convertToListTimeSpan(result, method.getName());
+            return convertToListTimeSpan(result, "getPushNotificationsRetries");
         }
         return getPushNotificationsRetries();
     }
+
+    @Override
+    protected Class<? extends KillbillConfig> getConfigClass() {
+        return NotificationConfig.class;
+    }
 }
diff --git a/profiles/killbill/src/main/resources/killbill-server.properties b/profiles/killbill/src/main/resources/killbill-server.properties
index 19f583a..3c2b298 100644
--- a/profiles/killbill/src/main/resources/killbill-server.properties
+++ b/profiles/killbill/src/main/resources/killbill-server.properties
@@ -22,7 +22,7 @@ org.killbill.jruby.context.version=RUBY2_0
 # KILLBILL GENERIC PROPERTIES
 #
 # Database config
-org.killbill.dao.url=jdbc:mysql://127.0.0.1:3306/killbill_0_17_x
+org.killbill.dao.url=jdbc:mysql://127.0.0.1:3306/killbill
 org.killbill.dao.user=root
 org.killbill.dao.password=root
 org.killbill.dao.logLevel=DEBUG
@@ -61,7 +61,7 @@ org.killbill.tenant.broadcast.rate=1s
 # PLUGIN SPECIFIC PROPERTIES
 #
 # Database config (OSGI plugins)
-org.killbill.billing.osgi.dao.url=jdbc:mysql://127.0.0.1:3306/killbill_0_17_x
+org.killbill.billing.osgi.dao.url=jdbc:mysql://127.0.0.1:3306/killbill
 org.killbill.billing.osgi.dao.user=root
 org.killbill.billing.osgi.dao.password=root
 
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
index f2f8f2c..6b13996 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
@@ -22,11 +22,11 @@ import java.math.BigDecimal;
 import java.util.Comparator;
 import java.util.List;
 
+import org.killbill.billing.client.KillBillClientException;
 import org.killbill.billing.client.model.Account;
 import org.killbill.billing.client.model.Invoice;
 import org.killbill.billing.client.model.InvoicePayment;
 import org.killbill.billing.client.model.Invoices;
-import org.killbill.billing.client.model.Payment;
 import org.killbill.billing.client.model.Tags;
 import org.killbill.billing.util.tag.ControlTagType;
 import org.testng.Assert;
@@ -42,9 +42,9 @@ public class TestOverdue extends TestJaxrsBase {
     @Test(groups = "slow", description = "Upload and retrieve a per tenant overdue config")
     public void testMultiTenantOverdueConfig() throws Exception {
         final String overdueConfigPath = Resources.getResource("overdue.xml").getPath();
-        killBillClient.uploadXMLOverdueConfig(overdueConfigPath, createdBy, reason, comment);
+        killBillClient.uploadXMLOverdueConfig(overdueConfigPath, requestOptions);
 
-        final String overdueConfig = killBillClient.getXMLOverdueConfig();
+        final String overdueConfig = killBillClient.getXMLOverdueConfig(requestOptions);
         Assert.assertNotNull(overdueConfig);
     }
 
@@ -54,29 +54,29 @@ public class TestOverdue extends TestJaxrsBase {
         final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
 
         // Get the invoices
-        final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+        final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
         // 2 invoices but look for the non zero dollar one
         assertEquals(invoices.size(), 2);
 
         // We're still clear - see the configuration
-        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
+        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
 
         clock.addDays(30);
         crappyWaitForLackOfProperSynchonization();
-        Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD1");
+        Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD1");
 
         clock.addDays(10);
         crappyWaitForLackOfProperSynchonization();
-        Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD2");
+        Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD2");
 
         clock.addDays(10);
         crappyWaitForLackOfProperSynchonization();
-        Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getName(), "OD3");
+        Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD3");
 
         // Post external payments, paying the most recent invoice first: this is to avoid a race condition where
         // a refresh overdue notification kicks in after the first payment, which makes the account goes CLEAR and
         // triggers an AUTO_INVOICE_OFF tag removal (hence adjustment of the other invoices balance).
-        final Invoices invoicesForAccount = killBillClient.getInvoicesForAccount(accountJson.getAccountId());
+        final Invoices invoicesForAccount = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
         final List<Invoice> mostRecentInvoiceFirst = Ordering.<Invoice>from(new Comparator<Invoice>() {
             @Override
             public int compare(final Invoice invoice1, final Invoice invoice2) {
@@ -90,7 +90,7 @@ public class TestOverdue extends TestJaxrsBase {
                 invoicePayment.setPurchasedAmount(invoice.getAmount());
                 invoicePayment.setAccountId(accountJson.getAccountId());
                 invoicePayment.setTargetInvoiceId(invoice.getInvoiceId());
-                killBillClient.createInvoicePayment(invoicePayment, true, createdBy, reason, comment);
+                killBillClient.createInvoicePayment(invoicePayment, true, requestOptions);
             }
         }
 
@@ -98,7 +98,7 @@ public class TestOverdue extends TestJaxrsBase {
         crappyWaitForLackOfProperSynchonization();
 
         // Verify we're in clear state
-        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
+        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
     }
 
     @Test(groups = "slow", description = "Allow overdue condition by control tag defined in overdue config xml file")
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
index 0baeb5e..16f4c35 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestPayment.java
@@ -237,7 +237,7 @@ public class TestPayment extends TestJaxrsBase {
                                                     .withComment(comment)
                                                     .withQueryParams(queryParams).build();
 
-        Payments payments = killBillClient.searchPayments("1", 0L, 100L, AuditLevel.NONE, inputOptions);
+        Payments payments = killBillClient.searchPayments("", 0L, 100L, AuditLevel.NONE, inputOptions);
 
         Assert.assertNotNull(payments.get(0).getPaymentAttempts());
         Assert.assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "RETRIED");
diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml
index 27999e1..b71057c 100644
--- a/profiles/killpay/pom.xml
+++ b/profiles/killpay/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killpay</artifactId>

profiles/pom.xml 2(+1 -1)

diff --git a/profiles/pom.xml b/profiles/pom.xml
index b2e5cfe..6b436d0 100644
--- a/profiles/pom.xml
+++ b/profiles/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles</artifactId>
diff --git a/subscription/pom.xml b/subscription/pom.xml
index 9998aa4..091fbeb 100644
--- a/subscription/pom.xml
+++ b/subscription/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-subscription</artifactId>
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index dbf1446..fcc771d 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -37,7 +37,6 @@ import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingAlignment;
-import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.CatalogService;
@@ -170,6 +169,18 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
             }
 
             final DefaultSubscriptionBase baseSubscription = (DefaultSubscriptionBase) dao.getBaseSubscription(bundleId, context);
+
+            // verify the number of subscriptions (of the same kind) allowed per bundle
+            if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
+                if (plan.getPlansAllowedInBundle() != -1
+                    && plan.getPlansAllowedInBundle() > 0
+                    && addonUtils.countExistingAddOnsWithSamePlanName(getSubscriptionsForBundle(bundleId, null, context), plan.getName())
+                       >= plan.getPlansAllowedInBundle()) {
+                    // a new ADD_ON subscription of the same plan can't be added because it has reached its limit by bundle
+                    throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, plan.getName());
+                }
+            }
+
             final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan, effectiveDate, context);
             return apiService.createPlan(new SubscriptionBuilder()
                                                  .setId(UUIDs.randomUUID())
@@ -201,6 +212,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
             }
 
             boolean first = true;
+            final List<SubscriptionBase> subscriptionsForBundle = getSubscriptionsForBundle(bundleId, null, context);
+
             for (EntitlementSpecifier entitlement : entitlements) {
 
                 final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
@@ -221,6 +234,18 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                     }
                 }
 
+                // verify the number of subscriptions (of the same kind) allowed per bundle and the existing ones
+                if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
+                    if (plan.getPlansAllowedInBundle() != -1 && plan.getPlansAllowedInBundle() > 0) {
+                        int existingAddOnsWithSamePlanName = addonUtils.countExistingAddOnsWithSamePlanName(subscriptionsForBundle, plan.getName());
+                        int currentAddOnsWithSamePlanName = countCurrentAddOnsWithSamePlanName(entitlements, catalog, plan.getName(), effectiveDate, callContext);
+                        if ((existingAddOnsWithSamePlanName + currentAddOnsWithSamePlanName) > plan.getPlansAllowedInBundle()) {
+                            // a new ADD_ON subscription of the same plan can't be added because it has reached its limit by bundle
+                            throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, plan.getName());
+                        }
+                    }
+                }
+
                 SubscriptionSpecifier subscription = new SubscriptionSpecifier();
                 subscription.setRealPriceList(plan.getPriceListName());
                 subscription.setEffectiveDate(effectiveDate);
@@ -250,6 +275,25 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
         }
     }
 
+    private int countCurrentAddOnsWithSamePlanName(final Iterable<EntitlementSpecifier> entitlements,
+                                                   final Catalog catalog, final String planName,
+                                                   final DateTime effectiveDate, final CallContext callContext) throws CatalogApiException {
+        int countCurrentAddOns = 0;
+        for (EntitlementSpecifier entitlement : entitlements) {
+            final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
+            final PlanPhasePriceOverridesWithCallContext overridesWithContext =
+                    new DefaultPlanPhasePriceOverridesWithCallContext(entitlement.getOverrides(), callContext);
+            final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
+
+            if (plan.getName().equalsIgnoreCase(planName)
+                && plan.getProduct().getCategory() != null
+                && ProductCategory.ADD_ON.equals(plan.getProduct().getCategory())) {
+                countCurrentAddOns++;
+            }
+        }
+        return countCurrentAddOns;
+    }
+
     @Override
     public void cancelBaseSubscriptions(final Iterable<SubscriptionBase> subscriptions, final BillingActionPolicy policy, final InternalCallContext context) throws SubscriptionBaseApiException {
         apiService.cancelWithPolicyNoValidation(Iterables.<SubscriptionBase, DefaultSubscriptionBase>transform(subscriptions,
@@ -464,8 +508,26 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                                                      final PlanSpecifier spec,
                                                      final DateTime requestedDateWithMs,
                                                      final BillingActionPolicy requestedPolicy,
-                                                     final InternalTenantContext context) throws SubscriptionBaseApiException {
+                                                     final List<PlanPhasePriceOverride> overrides,
+                                                     final InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException {
         final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
+        final CallContext callContext = internalCallContextFactory.createCallContext(context);
+
+        // verify the number of subscriptions (of the same kind) allowed per bundle
+        final Catalog catalog = catalogService.getFullCatalog(true, true, context);
+        final DateTime now = clock.getUTCNow();
+        final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+        final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, callContext);
+        final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
+        if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
+            if (plan.getPlansAllowedInBundle() != -1
+                && plan.getPlansAllowedInBundle() > 0
+                && addonUtils.countExistingAddOnsWithSamePlanName(getSubscriptionsForBundle(subscription.getBundleId(), null, context), plan.getName())
+                   >= plan.getPlansAllowedInBundle()) {
+                // the plan can be changed to the new value, because it has reached its limit by bundle
+                throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, plan.getName());
+            }
+        }
         return apiService.dryRunChangePlan((DefaultSubscriptionBase) subscription, spec, requestedDateWithMs, requestedPolicy, tenantContext);
     }
 
@@ -534,13 +596,18 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
         List<SubscriptionBaseEvent> dryRunEvents = null;
         try {
             final PlanPhaseSpecifier inputSpec = dryRunArguments.getPlanPhaseSpecifier();
+            final boolean isInputSpecNullOrEmpty = inputSpec == null ||
+                                                   (inputSpec.getPlanName() == null && inputSpec.getProductName() == null && inputSpec.getBillingPeriod() == null);
             final Catalog catalog = catalogService.getFullCatalog(true, true, context);
 
-            final PlanPhasePriceOverridesWithCallContext overridesWithContext = null; // TODO not supported to dryRun with custom price
-            final Plan plan = (inputSpec != null && inputSpec.getProductName() != null && inputSpec.getBillingPeriod() != null) ?
-                              catalog.createOrFindPlan(inputSpec, overridesWithContext, utcNow) : null;
+            // Create an overridesWithContext with a null context to indicate this is dryRun and no price overriden plan should be created.
+            final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(dryRunArguments.getPlanPhasePriceOverrides(), null);
+            final Plan plan = isInputSpecNullOrEmpty ?
+                              null :
+                              catalog.createOrFindPlan(inputSpec, overridesWithContext, utcNow);
             final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context);
 
+
             if (dryRunArguments != null) {
                 switch (dryRunArguments.getAction()) {
                     case START_BILLING:
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index cffcd6b..0fee918 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -394,6 +394,16 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, context);
         final Plan newPlan = catalogService.getFullCatalog(true, true, internalCallContext).createOrFindPlan(spec, overridesWithContext, effectiveDate, subscription.getStartDate());
 
+        if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(newPlan.getProduct().getCategory().toString())) {
+            if (newPlan.getPlansAllowedInBundle() != -1
+                && newPlan.getPlansAllowedInBundle() > 0
+                && addonUtils.countExistingAddOnsWithSamePlanName(dao.getSubscriptions(subscription.getBundleId(), null, internalCallContext), newPlan.getName())
+                   >= newPlan.getPlansAllowedInBundle()) {
+                // the plan can be changed to the new value, because it has reached its limit by bundle
+                throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, newPlan.getName());
+            }
+        }
+
         if (newPlan.getProduct().getCategory() != subscription.getCategory()) {
             throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_INVALID, subscription.getId());
         }
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
index 3a55506..7098709 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
@@ -16,6 +16,8 @@
 
 package org.killbill.billing.subscription.engine.addon;
 
+import java.util.List;
+
 import org.joda.time.DateTime;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.callcontext.InternalTenantContext;
@@ -23,7 +25,9 @@ import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.CatalogService;
 import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 import org.killbill.billing.subscription.exceptions.SubscriptionBaseError;
@@ -121,4 +125,16 @@ public class AddonUtils {
         }
         return false;
     }
+
+    public int countExistingAddOnsWithSamePlanName(final List<SubscriptionBase> subscriptionsForBundle, final String planName) {
+        int countExistingAddOns = 0;
+        for (SubscriptionBase subscription : subscriptionsForBundle) {
+            if (subscription.getCurrentPlan().getName().equalsIgnoreCase(planName)
+                && subscription.getLastActiveProduct().getCategory() != null
+                && ProductCategory.ADD_ON.equals(subscription.getLastActiveProduct().getCategory())) {
+                countExistingAddOns++;
+            }
+        }
+        return countExistingAddOns;
+    }
 }

tenant/pom.xml 2(+1 -1)

diff --git a/tenant/pom.xml b/tenant/pom.xml
index 6b10bda..0dea385 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-tenant</artifactId>

usage/pom.xml 2(+1 -1)

diff --git a/usage/pom.xml b/usage/pom.xml
index 594fe85..4b86981 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-usage</artifactId>

util/pom.xml 2(+1 -1)

diff --git a/util/pom.xml b/util/pom.xml
index 524e002..c1ab5a4 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.17.3-SNAPSHOT</version>
+        <version>0.17.5-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-util</artifactId>
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 87f0b0a..39562ee 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
@@ -18,9 +18,12 @@
 package org.killbill.billing.util.config.tenant;
 
 import java.lang.reflect.Method;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.config.definition.KillbillConfig;
 import org.skife.config.Config;
 import org.skife.config.Separator;
 import org.skife.config.TimeSpan;
@@ -31,6 +34,7 @@ import com.google.common.collect.Iterables;
 
 public abstract class MultiTenantConfigBase {
 
+    private final Map<String, Method> methodsCache = new HashMap<String, Method>();
     protected final CacheConfig cacheConfig;
 
     private final static Function<String, Integer> INT_CONVERTER = new Function<String, Integer>() {
@@ -107,5 +111,23 @@ public abstract class MultiTenantConfigBase {
         return ImmutableList.copyOf(value.split(separator == null ? Separator.DEFAULT : separator.value()));
     }
 
-    protected abstract Method getConfigStaticMethod(final String methodName);
+    protected Method getConfigStaticMethod(final String methodName) {
+        Method method = methodsCache.get(methodName);
+        if (method == null) {
+            synchronized (methodsCache) {
+                method = methodsCache.get(methodName);
+                if (method == null) {
+                    try {
+                        method = getConfigClass().getMethod(methodName, InternalTenantContext.class);
+                        methodsCache.put(methodName, method);
+                    } catch (final NoSuchMethodException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            }
+        }
+        return method;
+    }
+
+    protected abstract Class<? extends KillbillConfig> getConfigClass();
 }
diff --git a/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java b/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java
index 3973932..7ae7360 100644
--- a/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java
+++ b/util/src/main/java/org/killbill/billing/util/security/AnnotationHierarchicalResolver.java
@@ -18,6 +18,7 @@ package org.killbill.billing.util.security;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.WeakHashMap;
 
@@ -26,13 +27,26 @@ import org.apache.shiro.aop.MethodInvocation;
 
 public class AnnotationHierarchicalResolver implements AnnotationResolver {
 
+    private final Map<String, Annotation> methodToAnnotation = new HashMap<String, Annotation>();
+
     @Override
     public Annotation getAnnotation(final MethodInvocation mi, final Class<? extends Annotation> clazz) {
         return getAnnotationFromMethod(mi.getMethod(), clazz);
     }
 
     public Annotation getAnnotationFromMethod(final Method method, final Class<? extends Annotation> clazz) {
-        return findAnnotation(method, clazz);
+        final String key = method.toString();
+        Annotation annotation = methodToAnnotation.get(key);
+        if (annotation == null) {
+            synchronized (methodToAnnotation) {
+                annotation = methodToAnnotation.get(key);
+                if (annotation == null) {
+                    annotation = findAnnotation(method, clazz);
+                    methodToAnnotation.put(key, annotation);
+                }
+            }
+        }
+        return annotation;
     }
 
     // The following comes from spring-core (AnnotationUtils) to handle annotations on interfaces