killbill-aplcache

Changes

Details

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/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/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..4f93cad
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
@@ -0,0 +1,110 @@
+/*
+ * 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.catalog.api.TimeUnit;
+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.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;
+
+    @JsonCreator
+    public OverdueConditionJson(@JsonProperty("timeSinceEarliestUnpaidInvoiceEqualsOrExceeds") final DurationJson timeSinceEarliestUnpaidInvoiceEqualsOrExceeds,
+                                @JsonProperty("controlTagInclusion") final ControlTagType controlTagInclusion,
+                                @JsonProperty("controlTagExclusion") final ControlTagType controlTagExclusion) {
+        this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+        this.controlTagInclusion = controlTagInclusion;
+        this.controlTagExclusion = controlTagExclusion;
+    }
+
+    public OverdueConditionJson(final OverdueCondition overdueCondition) {
+        this.timeSinceEarliestUnpaidInvoiceEqualsOrExceeds = new DurationJson(overdueCondition.getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds());
+        this.controlTagInclusion = overdueCondition.getInclusionControlTagType();
+        this.controlTagExclusion = overdueCondition.getExclusionControlTagType();
+    }
+
+    public DurationJson getTimeSinceEarliestUnpaidInvoiceEqualsOrExceeds() {
+        return timeSinceEarliestUnpaidInvoiceEqualsOrExceeds;
+    }
+
+    public ControlTagType getControlTagInclusion() {
+        return controlTagInclusion;
+    }
+
+    public ControlTagType getControlTagExclusion() {
+        return controlTagExclusion;
+    }
+
+    @Override
+    public String toString() {
+        return "OverdueConditionJson{" +
+               "timeSinceEarliestUnpaidInvoiceEqualsOrExceeds=" + timeSinceEarliestUnpaidInvoiceEqualsOrExceeds +
+               ", controlTagInclusion=" + controlTagInclusion +
+               ", controlTagExclusion=" + controlTagExclusion +
+               '}';
+    }
+
+    @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;
+        }
+        return controlTagExclusion == that.controlTagExclusion;
+
+    }
+
+    @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);
+        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());
+        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..f59c110
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueJson.java
@@ -0,0 +1,125 @@
+/*
+ * 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.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 toOverdueConfig(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;
+        for (final OverdueStateConfigJson cur : input.getOverdueStates()) {
+            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((new DefaultDuration()).setUnit(TimeUnit.DAYS).setNumber(cur.getAutoReevaluationIntervalDays()));
+            state.setCondition(OverdueConditionJson.toOverdueCondition(cur.getCondition()));
+            states[i++] = state;
+        }
+        overdueStateAccount.setAccountOverdueStates(states);
+        overdueStateAccount.setInitialReevaluationInterval(null);
+        return result;
+    }
+}
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..3ae8ae6 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
@@ -20,6 +20,8 @@ package org.killbill.billing.jaxrs.resources;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
@@ -28,14 +30,24 @@ import javax.ws.rs.HeaderParam;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
 
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.VersionedCatalog;
+import org.killbill.billing.catalog.api.StaticCatalog;
+import org.killbill.billing.jaxrs.json.CatalogJson;
+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.DefaultOverdueApi;
 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 +66,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 +93,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 +119,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.toOverdueConfig(overdueJson);
+        ((DefaultOverdueApi)overdueApi).uploadOverdueConfig(overdueConfig, callContext);
+        return uriBuilder.buildResponse(uriInfo, OverdueResource.class, null, null);
+    }
+
 }
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..2364cf4 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 {
 
@@ -76,6 +78,15 @@ public class DefaultOverdueApi implements OverdueApi {
         }
     }
 
+    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);
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..af49f62 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
@@ -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>
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 ad3d7c7..f2165f8 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
@@ -588,13 +588,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: