killbill-aplcache

entitlement: bug fixes in DefaultSubscriptionBundleTimeline Remove

12/6/2013 2:22:41 PM

Details

diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
index 54b1a92..df82f4d 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
@@ -25,15 +25,15 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
-import org.joda.time.LocalDate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -95,7 +95,6 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
     // - blocking state events occur at various level (account, bundle and subscription) so for higher level, we need to dispatch that on each subscription.
     //
     private List<SubscriptionEvent> computeEvents(final Collection<Entitlement> entitlements, final List<BlockingState> allBlockingStates, final DateTimeZone accountTimeZone) {
-
         // Extract ids for all entitlement in the list
         final Set<UUID> allEntitlementUUIDs = new TreeSet(Collections2.transform(entitlements, new Function<Entitlement, UUID>() {
             @Override
@@ -152,10 +151,33 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
             final int index = insertFromBlockingEvent(accountTimeZone, allEntitlementUUIDs, result, bs, bs.getEffectiveDate(), newEvents);
             insertAfterIndex(result, newEvents, index);
         }
+
         reOrderSubscriptionEventsOnSameDateByType(result);
+
+        removeOverlappingSubscriptionEvents(result);
+
         return result;
     }
 
+    // Make sure the argument supports the remove operation - hence expect a LinkedList, not a List
+    private void removeOverlappingSubscriptionEvents(final LinkedList<SubscriptionEvent> events) {
+        final Iterator<SubscriptionEvent> iterator = events.iterator();
+        final Map<String, DefaultSubscriptionEvent> prevPerService = new HashMap<String, DefaultSubscriptionEvent>();
+        while (iterator.hasNext()) {
+            final DefaultSubscriptionEvent current = (DefaultSubscriptionEvent) iterator.next();
+            final DefaultSubscriptionEvent prev = prevPerService.get(current.getServiceName());
+            if (prev != null) {
+                if (current.overlaps(prev)) {
+                    iterator.remove();
+                } else {
+                    prevPerService.put(current.getServiceName(), current);
+                }
+            } else {
+                prevPerService.put(current.getServiceName(), current);
+            }
+        }
+    }
+
     //
     // All events have been inserted and should be at the right place, except that we want to ensure that events for a given subscription,
     // and for a given time are ordered by SubscriptionEventType.
@@ -280,7 +302,6 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
 
         // For each target compute the new events that should be inserted in the stream
         for (final UUID target : targetEntitlementIds) {
-
             final SubscriptionEvent[] prevNext = findPrevNext(result, target, curInsertion);
             final TargetState curTargetState = targetStates.get(target);
 
@@ -339,36 +360,8 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                 }
             }
         }
-        sanitizeForBaseRecreateEvents(result);
-        return result;
-    }
 
-    //
-    // Old version of code would use CANCEL/RE_CREATE to simulate PAUSE_BILLING/RESUME_BILLING
-    // (Relies on the assumption that there is no blocking_state event matching that CACNEL event so:
-    // 1. The STOP_BILLING (coming from the row CANCEL event) should be transformed into a PAUSE_BILLING
-    // 2. We also add a PAUSE_ENTITLEMENT at the same time as the PAUSE_BILLING
-    //
-    private void sanitizeForBaseRecreateEvents(final LinkedList<SubscriptionEvent> input) {
-        final Collection<UUID> guiltyEntitlementIds = new TreeSet<UUID>();
-        final ListIterator<SubscriptionEvent> it = input.listIterator(input.size());
-        while (it.hasPrevious()) {
-            final SubscriptionEvent cur = it.previous();
-            if (cur.getSubscriptionEventType() == SubscriptionEventType.RESUME_BILLING) {
-                guiltyEntitlementIds.add(cur.getEntitlementId());
-                continue;
-            }
-            if (cur.getSubscriptionEventType() == SubscriptionEventType.STOP_BILLING &&
-                guiltyEntitlementIds.contains(cur.getEntitlementId())) {
-                guiltyEntitlementIds.remove(cur.getEntitlementId());
-                final SubscriptionEvent correctedBillingEvent = new DefaultSubscriptionEvent((DefaultSubscriptionEvent) cur, SubscriptionEventType.PAUSE_BILLING);
-                it.set(correctedBillingEvent);
-
-                // Old versions of the code won't have an associated event in blocking_states - we need to add one on the fly
-                final SubscriptionEvent correctedEntitlementEvent = new DefaultSubscriptionEvent((DefaultSubscriptionEvent) cur, SubscriptionEventType.PAUSE_ENTITLEMENT);
-                it.add(correctedEntitlementEvent);
-            }
-        }
+        return result;
     }
 
     private void insertSubscriptionEvent(final SubscriptionEvent event, final List<SubscriptionEvent> result) {
@@ -405,7 +398,66 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         result.add(index, event);
     }
 
-    private SubscriptionEvent toSubscriptionEvent(final SubscriptionEvent prev, final SubscriptionEvent next, final UUID entitlementId, final BlockingState in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
+    private SubscriptionEvent toSubscriptionEvent(@Nullable final SubscriptionEvent prev, @Nullable final SubscriptionEvent next,
+                                                  final UUID entitlementId, final BlockingState in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
+        final Product prevProduct;
+        final Plan prevPlan;
+        final PlanPhase prevPlanPhase;
+        final PriceList prevPriceList;
+        final BillingPeriod prevBillingPeriod;
+        // Enforce prev = null for start events
+        if (prev == null || SubscriptionEventType.START_ENTITLEMENT.equals(eventType) || SubscriptionEventType.START_BILLING.equals(eventType)) {
+            prevProduct = null;
+            prevPlan = null;
+            prevPlanPhase = null;
+            prevPriceList = null;
+            prevBillingPeriod = null;
+        } else {
+            // We look for the next for the 'prev' meaning we we are headed to, but if this is null -- for example on cancellation we get the prev which gives the correct state.
+            prevProduct = (prev.getNextProduct() != null ? prev.getNextProduct() : prev.getPrevProduct());
+            prevPlan = (prev.getNextPlan() != null ? prev.getNextPlan() : prev.getPrevPlan());
+            prevPlanPhase = (prev.getNextPhase() != null ? prev.getNextPhase() : prev.getPrevPhase());
+            prevPriceList = (prev.getNextPriceList() != null ? prev.getNextPriceList() : prev.getPrevPriceList());
+            prevBillingPeriod = (prev.getNextBillingPeriod() != null ? prev.getNextBillingPeriod() : prev.getPrevBillingPeriod());
+        }
+
+        final Product nextProduct;
+        final Plan nextPlan;
+        final PlanPhase nextPlanPhase;
+        final PriceList nextPriceList;
+        final BillingPeriod nextBillingPeriod;
+        if (SubscriptionEventType.PAUSE_ENTITLEMENT.equals(eventType) || SubscriptionEventType.PAUSE_BILLING.equals(eventType) ||
+            SubscriptionEventType.RESUME_ENTITLEMENT.equals(eventType) || SubscriptionEventType.RESUME_BILLING.equals(eventType) ||
+            (SubscriptionEventType.SERVICE_STATE_CHANGE.equals(eventType) && (prev == null || (!SubscriptionEventType.STOP_ENTITLEMENT.equals(prev.getSubscriptionEventType()) && !SubscriptionEventType.STOP_BILLING.equals(prev.getSubscriptionEventType()))))) {
+            // Enforce next = prev for pause/resume events as well as service changes
+            nextProduct = prevProduct;
+            nextPlan = prevPlan;
+            nextPlanPhase = prevPlanPhase;
+            nextPriceList = prevPriceList;
+            nextBillingPeriod = prevBillingPeriod;
+        } else if (next == null) {
+            // Enforce next = null for stop events
+            if (prev == null || SubscriptionEventType.STOP_ENTITLEMENT.equals(eventType) || SubscriptionEventType.STOP_BILLING.equals(eventType)) {
+                nextProduct = null;
+                nextPlan = null;
+                nextPlanPhase = null;
+                nextPriceList = null;
+                nextBillingPeriod = null;
+            } else {
+                nextProduct = prev.getNextProduct();
+                nextPlan = prev.getNextPlan();
+                nextPlanPhase = prev.getNextPhase();
+                nextPriceList = prev.getNextPriceList();
+                nextBillingPeriod = prev.getNextBillingPeriod();
+            }
+        } else {
+            nextProduct = next.getNextProduct();
+            nextPlan = next.getNextPlan();
+            nextPlanPhase = next.getNextPhase();
+            nextPriceList = next.getNextPriceList();
+            nextBillingPeriod = next.getNextBillingPeriod();
+        }
+
         return new DefaultSubscriptionEvent(in.getId(),
                                             entitlementId,
                                             in.getEffectiveDate(),
@@ -415,17 +467,16 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                                             in.isBlockBilling(),
                                             in.getService(),
                                             in.getStateName(),
-                                            // We look for the next for the 'prev' meaning we we are headed to, but if this is null -- for example on cancellation we get the prev which gives the correct state.
-                                            prev != null ? (prev.getNextProduct() != null ? prev.getNextProduct() : prev.getPrevProduct()) : null,
-                                            prev != null ? (prev.getNextPlan() != null ? prev.getNextPlan() : prev.getPrevPlan()) : null,
-                                            prev != null ? (prev.getNextPhase() != null ? prev.getNextPhase() : prev.getPrevPhase()) : null,
-                                            prev != null ? (prev.getNextPriceList() != null ? prev.getNextPriceList() : prev.getPrevPriceList()) : null,
-                                            prev != null ? (prev.getNextBillingPeriod() != null ? prev.getNextBillingPeriod() : prev.getPrevBillingPeriod()) : null,
-                                            next != null ? next.getPrevProduct() : null,
-                                            next != null ? next.getPrevPlan() : null,
-                                            next != null ? next.getPrevPhase() : null,
-                                            next != null ? next.getPrevPriceList() : null,
-                                            next != null ? next.getPrevBillingPeriod() : null,
+                                            prevProduct,
+                                            prevPlan,
+                                            prevPlanPhase,
+                                            prevPriceList,
+                                            prevBillingPeriod,
+                                            nextProduct,
+                                            nextPlan,
+                                            nextPlanPhase,
+                                            nextPriceList,
+                                            nextBillingPeriod,
                                             in.getCreatedDate(),
                                             accountTimeZone);
     }
@@ -487,11 +538,6 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                 return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.STOP_BILLING);
             case PHASE:
                 return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.PHASE);
-            // This is the old way of pausing billing; not used any longer, but kept for compatibility reason. We return both RESUME_ENTITLEMENT and RESUME_BILLING
-            // and will rely on the sanitizeForBaseRecreateEvents method to transform the STOP_BILLING (coming from CANCEL) into the correct events.
-            //
-            case RE_CREATE:
-                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.RESUME_ENTITLEMENT, SubscriptionEventType.RESUME_BILLING);
             /*
              * Those can be ignored:
              */
@@ -572,7 +618,6 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         // From the current state of that subscription, compute the effect of the new state based on the incoming blockingState event
         //
         private List<SubscriptionEventType> addStateAndReturnEventTypes(final BlockingState bs) {
-
             // Turn off isBlockedEntitlement and isBlockedBilling if there was not start event
             final BlockingState fixedBlockingState = new DefaultBlockingState(bs.getBlockedId(),
                                                                               bs.getType(),
@@ -630,308 +675,4 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
             return aggrBefore;
         }
     }
-
-    protected static final class DefaultSubscriptionEvent implements SubscriptionEvent {
-
-        private final UUID id;
-        private final UUID entitlementId;
-        private final DateTime effectiveDate;
-        private final DateTime requestedDate;
-        private final SubscriptionEventType eventType;
-        private final boolean isBlockingEntitlement;
-        private final boolean isBlockingBilling;
-        private final String serviceName;
-        private final String serviceStateName;
-        private final Product prevProduct;
-        private final Plan prevPlan;
-        private final PlanPhase prevPlanPhase;
-        private final PriceList prevPriceList;
-        private final BillingPeriod prevBillingPeriod;
-        private final Product nextProduct;
-        private final Plan nextPlan;
-        private final PlanPhase nextPlanPhase;
-        private final PriceList nextPriceList;
-        private final BillingPeriod nextBillingPeriod;
-        private final DateTime createdDate;
-        private final DateTimeZone accountTimeZone;
-
-        public DefaultSubscriptionEvent(final UUID id,
-                                        final UUID entitlementId,
-                                        final DateTime effectiveDate,
-                                        final DateTime requestedDate,
-                                        final SubscriptionEventType eventType,
-                                        final boolean blockingEntitlement,
-                                        final boolean blockingBilling,
-                                        final String serviceName,
-                                        final String serviceStateName,
-                                        final Product prevProduct,
-                                        final Plan prevPlan,
-                                        final PlanPhase prevPlanPhase,
-                                        final PriceList prevPriceList,
-                                        final BillingPeriod prevBillingPeriod,
-                                        final Product nextProduct,
-                                        final Plan nextPlan,
-                                        final PlanPhase nextPlanPhase,
-                                        final PriceList nextPriceList,
-                                        final BillingPeriod nextBillingPeriod,
-                                        final DateTime createDate,
-                                        final DateTimeZone accountTimeZone) {
-            this.id = id;
-            this.entitlementId = entitlementId;
-            this.effectiveDate = effectiveDate;
-            this.requestedDate = requestedDate;
-            this.eventType = eventType;
-            this.isBlockingEntitlement = blockingEntitlement;
-            this.isBlockingBilling = blockingBilling;
-            this.serviceName = serviceName;
-            this.serviceStateName = serviceStateName;
-            this.prevProduct = prevProduct;
-            this.prevPlan = prevPlan;
-            this.prevPlanPhase = prevPlanPhase;
-            this.prevPriceList = prevPriceList;
-            this.prevBillingPeriod = prevBillingPeriod;
-            this.nextProduct = nextProduct;
-            this.nextPlan = nextPlan;
-            this.nextPlanPhase = nextPlanPhase;
-            this.nextPriceList = nextPriceList;
-            this.nextBillingPeriod = nextBillingPeriod;
-            this.createdDate = createDate;
-            this.accountTimeZone = accountTimeZone;
-        }
-
-        private DefaultSubscriptionEvent(final DefaultSubscriptionEvent copy, final SubscriptionEventType newEventType) {
-            this(copy.getId(),
-                 copy.getEntitlementId(),
-                 copy.getEffectiveDateTime(),
-                 copy.getRequestedDateTime(),
-                 newEventType,
-                 copy.isBlockedEntitlement(),
-                 copy.isBlockedBilling(),
-                 copy.getServiceName(),
-                 copy.getServiceStateName(),
-                 copy.getPrevProduct(),
-                 copy.getPrevPlan(),
-                 copy.getPrevPhase(),
-                 copy.getPrevPriceList(),
-                 copy.getPrevBillingPeriod(),
-                 copy.getNextProduct(),
-                 copy.getNextPlan(),
-                 copy.getNextPhase(),
-                 copy.getNextPriceList(),
-                 copy.getNextBillingPeriod(),
-                 copy.getCreatedDate(),
-                 copy.getAccountTimeZone());
-        }
-
-        public DateTimeZone getAccountTimeZone() {
-            return accountTimeZone;
-        }
-
-        public DateTime getEffectiveDateTime() {
-            return effectiveDate;
-        }
-
-        public DateTime getRequestedDateTime() {
-            return requestedDate;
-        }
-
-        @Override
-        public UUID getId() {
-            return id;
-        }
-
-        @Override
-        public UUID getEntitlementId() {
-            return entitlementId;
-        }
-
-        @Override
-        public LocalDate getEffectiveDate() {
-            return effectiveDate != null ? new LocalDate(effectiveDate, accountTimeZone) : null;
-        }
-
-        @Override
-        public LocalDate getRequestedDate() {
-            return requestedDate != null ? new LocalDate(requestedDate, accountTimeZone) : null;
-        }
-
-        @Override
-        public SubscriptionEventType getSubscriptionEventType() {
-            return eventType;
-        }
-
-        @Override
-        public boolean isBlockedBilling() {
-            return isBlockingBilling;
-        }
-
-        @Override
-        public boolean isBlockedEntitlement() {
-            return isBlockingEntitlement;
-        }
-
-        @Override
-        public String getServiceName() {
-            return serviceName;
-        }
-
-        @Override
-        public String getServiceStateName() {
-            return serviceStateName;
-        }
-
-        @Override
-        public Product getPrevProduct() {
-            return prevProduct;
-        }
-
-        @Override
-        public Plan getPrevPlan() {
-            return prevPlan;
-        }
-
-        @Override
-        public PlanPhase getPrevPhase() {
-            return prevPlanPhase;
-        }
-
-        @Override
-        public PriceList getPrevPriceList() {
-            return prevPriceList;
-        }
-
-        @Override
-        public BillingPeriod getPrevBillingPeriod() {
-            return prevBillingPeriod;
-        }
-
-        @Override
-        public Product getNextProduct() {
-            return nextProduct;
-        }
-
-        @Override
-        public Plan getNextPlan() {
-            return nextPlan;
-        }
-
-        @Override
-        public PlanPhase getNextPhase() {
-            return nextPlanPhase;
-        }
-
-        @Override
-        public PriceList getNextPriceList() {
-            return nextPriceList;
-        }
-
-        @Override
-        public BillingPeriod getNextBillingPeriod() {
-            return nextBillingPeriod;
-        }
-
-        public DateTime getCreatedDate() {
-            return createdDate;
-        }
-
-        @Override
-        public boolean equals(final Object o) {
-            if (this == o) {
-                return true;
-            }
-            if (o == null || getClass() != o.getClass()) {
-                return false;
-            }
-
-            final DefaultSubscriptionEvent that = (DefaultSubscriptionEvent) o;
-
-            if (isBlockingBilling != that.isBlockingBilling) {
-                return false;
-            }
-            if (isBlockingEntitlement != that.isBlockingEntitlement) {
-                return false;
-            }
-            if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) {
-                return false;
-            }
-            if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
-                return false;
-            }
-            if (entitlementId != null ? !entitlementId.equals(that.entitlementId) : that.entitlementId != null) {
-                return false;
-            }
-            if (eventType != that.eventType) {
-                return false;
-            }
-            if (id != null ? !id.equals(that.id) : that.id != null) {
-                return false;
-            }
-            if (nextBillingPeriod != that.nextBillingPeriod) {
-                return false;
-            }
-            if (nextPlan != null ? !nextPlan.equals(that.nextPlan) : that.nextPlan != null) {
-                return false;
-            }
-            if (nextPlanPhase != null ? !nextPlanPhase.equals(that.nextPlanPhase) : that.nextPlanPhase != null) {
-                return false;
-            }
-            if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
-                return false;
-            }
-            if (nextProduct != null ? !nextProduct.equals(that.nextProduct) : that.nextProduct != null) {
-                return false;
-            }
-            if (prevBillingPeriod != that.prevBillingPeriod) {
-                return false;
-            }
-            if (prevPlan != null ? !prevPlan.equals(that.prevPlan) : that.prevPlan != null) {
-                return false;
-            }
-            if (prevPlanPhase != null ? !prevPlanPhase.equals(that.prevPlanPhase) : that.prevPlanPhase != null) {
-                return false;
-            }
-            if (prevPriceList != null ? !prevPriceList.equals(that.prevPriceList) : that.prevPriceList != null) {
-                return false;
-            }
-            if (prevProduct != null ? !prevProduct.equals(that.prevProduct) : that.prevProduct != null) {
-                return false;
-            }
-            if (requestedDate != null ? !requestedDate.equals(that.requestedDate) : that.requestedDate != null) {
-                return false;
-            }
-            if (serviceName != null ? !serviceName.equals(that.serviceName) : that.serviceName != null) {
-                return false;
-            }
-            if (serviceStateName != null ? !serviceStateName.equals(that.serviceStateName) : that.serviceStateName != null) {
-                return false;
-            }
-
-            return true;
-        }
-
-        @Override
-        public int hashCode() {
-            int result = id != null ? id.hashCode() : 0;
-            result = 31 * result + (entitlementId != null ? entitlementId.hashCode() : 0);
-            result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
-            result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
-            result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
-            result = 31 * result + (isBlockingEntitlement ? 1 : 0);
-            result = 31 * result + (isBlockingBilling ? 1 : 0);
-            result = 31 * result + (serviceName != null ? serviceName.hashCode() : 0);
-            result = 31 * result + (serviceStateName != null ? serviceStateName.hashCode() : 0);
-            result = 31 * result + (prevProduct != null ? prevProduct.hashCode() : 0);
-            result = 31 * result + (prevPlan != null ? prevPlan.hashCode() : 0);
-            result = 31 * result + (prevPlanPhase != null ? prevPlanPhase.hashCode() : 0);
-            result = 31 * result + (prevPriceList != null ? prevPriceList.hashCode() : 0);
-            result = 31 * result + (prevBillingPeriod != null ? prevBillingPeriod.hashCode() : 0);
-            result = 31 * result + (nextProduct != null ? nextProduct.hashCode() : 0);
-            result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
-            result = 31 * result + (nextPlanPhase != null ? nextPlanPhase.hashCode() : 0);
-            result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
-            result = 31 * result + (nextBillingPeriod != null ? nextBillingPeriod.hashCode() : 0);
-            result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
-            return result;
-        }
-    }
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionEvent.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionEvent.java
new file mode 100644
index 0000000..fbd7ab6
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionEvent.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.entitlement.api;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.LocalDate;
+
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PriceList;
+import com.ning.billing.catalog.api.Product;
+
+public class DefaultSubscriptionEvent implements SubscriptionEvent {
+
+    private final UUID id;
+    private final UUID entitlementId;
+    private final DateTime effectiveDate;
+    private final DateTime requestedDate;
+    private final SubscriptionEventType eventType;
+    private final boolean isBlockingEntitlement;
+    private final boolean isBlockingBilling;
+    private final String serviceName;
+    private final String serviceStateName;
+    private final Product prevProduct;
+    private final Plan prevPlan;
+    private final PlanPhase prevPlanPhase;
+    private final PriceList prevPriceList;
+    private final BillingPeriod prevBillingPeriod;
+    private final Product nextProduct;
+    private final Plan nextPlan;
+    private final PlanPhase nextPlanPhase;
+    private final PriceList nextPriceList;
+    private final BillingPeriod nextBillingPeriod;
+    private final DateTime createdDate;
+    private final DateTimeZone accountTimeZone;
+
+    public DefaultSubscriptionEvent(final UUID id,
+                                    final UUID entitlementId,
+                                    final DateTime effectiveDate,
+                                    final DateTime requestedDate,
+                                    final SubscriptionEventType eventType,
+                                    final boolean blockingEntitlement,
+                                    final boolean blockingBilling,
+                                    final String serviceName,
+                                    final String serviceStateName,
+                                    final Product prevProduct,
+                                    final Plan prevPlan,
+                                    final PlanPhase prevPlanPhase,
+                                    final PriceList prevPriceList,
+                                    final BillingPeriod prevBillingPeriod,
+                                    final Product nextProduct,
+                                    final Plan nextPlan,
+                                    final PlanPhase nextPlanPhase,
+                                    final PriceList nextPriceList,
+                                    final BillingPeriod nextBillingPeriod,
+                                    final DateTime createDate,
+                                    final DateTimeZone accountTimeZone) {
+        this.id = id;
+        this.entitlementId = entitlementId;
+        this.effectiveDate = effectiveDate;
+        this.requestedDate = requestedDate;
+        this.eventType = eventType;
+        this.isBlockingEntitlement = blockingEntitlement;
+        this.isBlockingBilling = blockingBilling;
+        this.serviceName = serviceName;
+        this.serviceStateName = serviceStateName;
+        this.prevProduct = prevProduct;
+        this.prevPlan = prevPlan;
+        this.prevPlanPhase = prevPlanPhase;
+        this.prevPriceList = prevPriceList;
+        this.prevBillingPeriod = prevBillingPeriod;
+        this.nextProduct = nextProduct;
+        this.nextPlan = nextPlan;
+        this.nextPlanPhase = nextPlanPhase;
+        this.nextPriceList = nextPriceList;
+        this.nextBillingPeriod = nextBillingPeriod;
+        this.createdDate = createDate;
+        this.accountTimeZone = accountTimeZone;
+    }
+
+    public DefaultSubscriptionEvent(final DefaultSubscriptionEvent copy, final SubscriptionEventType newEventType) {
+        this(copy.getId(),
+             copy.getEntitlementId(),
+             copy.getEffectiveDateTime(),
+             copy.getRequestedDateTime(),
+             newEventType,
+             copy.isBlockedEntitlement(),
+             copy.isBlockedBilling(),
+             copy.getServiceName(),
+             copy.getServiceStateName(),
+             copy.getPrevProduct(),
+             copy.getPrevPlan(),
+             copy.getPrevPhase(),
+             copy.getPrevPriceList(),
+             copy.getPrevBillingPeriod(),
+             copy.getNextProduct(),
+             copy.getNextPlan(),
+             copy.getNextPhase(),
+             copy.getNextPriceList(),
+             copy.getNextBillingPeriod(),
+             copy.getCreatedDate(),
+             copy.getAccountTimeZone());
+    }
+
+    public DateTimeZone getAccountTimeZone() {
+        return accountTimeZone;
+    }
+
+    public DateTime getEffectiveDateTime() {
+        return effectiveDate;
+    }
+
+    public DateTime getRequestedDateTime() {
+        return requestedDate;
+    }
+
+    @Override
+    public UUID getId() {
+        return id;
+    }
+
+    @Override
+    public UUID getEntitlementId() {
+        return entitlementId;
+    }
+
+    @Override
+    public LocalDate getEffectiveDate() {
+        return effectiveDate != null ? new LocalDate(effectiveDate, accountTimeZone) : null;
+    }
+
+    @Override
+    public LocalDate getRequestedDate() {
+        return requestedDate != null ? new LocalDate(requestedDate, accountTimeZone) : null;
+    }
+
+    @Override
+    public SubscriptionEventType getSubscriptionEventType() {
+        return eventType;
+    }
+
+    @Override
+    public boolean isBlockedBilling() {
+        return isBlockingBilling;
+    }
+
+    @Override
+    public boolean isBlockedEntitlement() {
+        return isBlockingEntitlement;
+    }
+
+    @Override
+    public String getServiceName() {
+        return serviceName;
+    }
+
+    @Override
+    public String getServiceStateName() {
+        return serviceStateName;
+    }
+
+    @Override
+    public Product getPrevProduct() {
+        return prevProduct;
+    }
+
+    @Override
+    public Plan getPrevPlan() {
+        return prevPlan;
+    }
+
+    @Override
+    public PlanPhase getPrevPhase() {
+        return prevPlanPhase;
+    }
+
+    @Override
+    public PriceList getPrevPriceList() {
+        return prevPriceList;
+    }
+
+    @Override
+    public BillingPeriod getPrevBillingPeriod() {
+        return prevBillingPeriod;
+    }
+
+    @Override
+    public Product getNextProduct() {
+        return nextProduct;
+    }
+
+    @Override
+    public Plan getNextPlan() {
+        return nextPlan;
+    }
+
+    @Override
+    public PlanPhase getNextPhase() {
+        return nextPlanPhase;
+    }
+
+    @Override
+    public PriceList getNextPriceList() {
+        return nextPriceList;
+    }
+
+    @Override
+    public BillingPeriod getNextBillingPeriod() {
+        return nextBillingPeriod;
+    }
+
+    public DateTime getCreatedDate() {
+        return createdDate;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultSubscriptionEvent that = (DefaultSubscriptionEvent) o;
+
+        if (isBlockingBilling != that.isBlockingBilling) {
+            return false;
+        }
+        if (isBlockingEntitlement != that.isBlockingEntitlement) {
+            return false;
+        }
+        if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) {
+            return false;
+        }
+        if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+            return false;
+        }
+        if (entitlementId != null ? !entitlementId.equals(that.entitlementId) : that.entitlementId != null) {
+            return false;
+        }
+        if (eventType != that.eventType) {
+            return false;
+        }
+        if (id != null ? !id.equals(that.id) : that.id != null) {
+            return false;
+        }
+        if (nextBillingPeriod != that.nextBillingPeriod) {
+            return false;
+        }
+        if (nextPlan != null ? !nextPlan.equals(that.nextPlan) : that.nextPlan != null) {
+            return false;
+        }
+        if (nextPlanPhase != null ? !nextPlanPhase.equals(that.nextPlanPhase) : that.nextPlanPhase != null) {
+            return false;
+        }
+        if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
+            return false;
+        }
+        if (nextProduct != null ? !nextProduct.equals(that.nextProduct) : that.nextProduct != null) {
+            return false;
+        }
+        if (prevBillingPeriod != that.prevBillingPeriod) {
+            return false;
+        }
+        if (prevPlan != null ? !prevPlan.equals(that.prevPlan) : that.prevPlan != null) {
+            return false;
+        }
+        if (prevPlanPhase != null ? !prevPlanPhase.equals(that.prevPlanPhase) : that.prevPlanPhase != null) {
+            return false;
+        }
+        if (prevPriceList != null ? !prevPriceList.equals(that.prevPriceList) : that.prevPriceList != null) {
+            return false;
+        }
+        if (prevProduct != null ? !prevProduct.equals(that.prevProduct) : that.prevProduct != null) {
+            return false;
+        }
+        if (requestedDate != null ? !requestedDate.equals(that.requestedDate) : that.requestedDate != null) {
+            return false;
+        }
+        if (serviceName != null ? !serviceName.equals(that.serviceName) : that.serviceName != null) {
+            return false;
+        }
+        if (serviceStateName != null ? !serviceStateName.equals(that.serviceStateName) : that.serviceStateName != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public boolean overlaps(final DefaultSubscriptionEvent that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null || getClass() != that.getClass()) {
+            return false;
+        }
+
+        if (isBlockingBilling != that.isBlockingBilling) {
+            return false;
+        }
+        if (isBlockingEntitlement != that.isBlockingEntitlement) {
+            return false;
+        }
+        if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) < 0 : that.effectiveDate != null) {
+            return false;
+        }
+        if (entitlementId != null ? !entitlementId.equals(that.entitlementId) : that.entitlementId != null) {
+            return false;
+        }
+        if (eventType != that.eventType) {
+            return false;
+        }
+        if (nextBillingPeriod != that.nextBillingPeriod) {
+            return false;
+        }
+        if (nextPlan != null ? !nextPlan.equals(that.nextPlan) : that.nextPlan != null) {
+            return false;
+        }
+        if (nextPlanPhase != null ? !nextPlanPhase.equals(that.nextPlanPhase) : that.nextPlanPhase != null) {
+            return false;
+        }
+        if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
+            return false;
+        }
+        if (nextProduct != null ? !nextProduct.equals(that.nextProduct) : that.nextProduct != null) {
+            return false;
+        }
+        if (prevBillingPeriod != that.prevBillingPeriod) {
+            return false;
+        }
+        if (prevPlan != null ? !prevPlan.equals(that.prevPlan) : that.prevPlan != null) {
+            return false;
+        }
+        if (prevPlanPhase != null ? !prevPlanPhase.equals(that.prevPlanPhase) : that.prevPlanPhase != null) {
+            return false;
+        }
+        if (prevPriceList != null ? !prevPriceList.equals(that.prevPriceList) : that.prevPriceList != null) {
+            return false;
+        }
+        if (prevProduct != null ? !prevProduct.equals(that.prevProduct) : that.prevProduct != null) {
+            return false;
+        }
+        if (serviceName != null ? !serviceName.equals(that.serviceName) : that.serviceName != null) {
+            return false;
+        }
+        if (serviceStateName != null ? !serviceStateName.equals(that.serviceStateName) : that.serviceStateName != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = id != null ? id.hashCode() : 0;
+        result = 31 * result + (entitlementId != null ? entitlementId.hashCode() : 0);
+        result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+        result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
+        result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
+        result = 31 * result + (isBlockingEntitlement ? 1 : 0);
+        result = 31 * result + (isBlockingBilling ? 1 : 0);
+        result = 31 * result + (serviceName != null ? serviceName.hashCode() : 0);
+        result = 31 * result + (serviceStateName != null ? serviceStateName.hashCode() : 0);
+        result = 31 * result + (prevProduct != null ? prevProduct.hashCode() : 0);
+        result = 31 * result + (prevPlan != null ? prevPlan.hashCode() : 0);
+        result = 31 * result + (prevPlanPhase != null ? prevPlanPhase.hashCode() : 0);
+        result = 31 * result + (prevPriceList != null ? prevPriceList.hashCode() : 0);
+        result = 31 * result + (prevBillingPeriod != null ? prevBillingPeriod.hashCode() : 0);
+        result = 31 * result + (nextProduct != null ? nextProduct.hashCode() : 0);
+        result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
+        result = 31 * result + (nextPlanPhase != null ? nextPlanPhase.hashCode() : 0);
+        result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
+        result = 31 * result + (nextBillingPeriod != null ? nextBillingPeriod.hashCode() : 0);
+        result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
index cf93ba9..aa75652 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
@@ -44,6 +44,7 @@ import com.ning.billing.subscription.events.SubscriptionBaseEvent.EventType;
 import com.ning.billing.subscription.events.user.ApiEventType;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
 
 public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteNoDB {
 
@@ -311,81 +312,14 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.PHASE);
         assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
+        assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
+        assertNull(events.get(1).getPrevPhase());
         assertEquals(events.get(1).getNextPhase().getName(), "trial");
+        assertEquals(events.get(2).getPrevPhase().getName(), "trial");
         assertEquals(events.get(2).getNextPhase().getName(), "phase");
-        assertEquals(events.get(3).getNextPhase(), null);
-    }
-
-    @Test(groups = "fast")
-    public void testOneEntitlementWithRecreate() throws CatalogApiException {
-        clock.setDay(new LocalDate(2013, 1, 1));
-
-        final DateTimeZone accountTimeZone = DateTimeZone.UTC;
-        final UUID accountId = UUID.randomUUID();
-        final UUID bundleId = UUID.randomUUID();
-        final String externalKey = "foo";
-
-        final UUID entitlementId = UUID.randomUUID();
-
-        final List<SubscriptionBaseTransition> allTransitions = new ArrayList<SubscriptionBaseTransition>();
-
-        final DateTime requestedDate = new DateTime();
-        DateTime effectiveDate = new DateTime(2013, 1, 1, 15, 43, 25, 0, DateTimeZone.UTC);
-        final SubscriptionBaseTransition tr1 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CREATE, requestedDate, effectiveDate, clock.getUTCNow(), null, "trial");
-        allTransitions.add(tr1);
-
-        effectiveDate = effectiveDate.plusDays(30);
-        clock.addDays(30);
-        final SubscriptionBaseTransition tr2 = createTransition(entitlementId, EventType.PHASE, null, requestedDate, effectiveDate, clock.getUTCNow(), "trial", "phase");
-        allTransitions.add(tr2);
-
-        effectiveDate = effectiveDate.plusDays(15);
-        clock.addDays(15);
-        final SubscriptionBaseTransition tr3 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CANCEL, requestedDate, effectiveDate, clock.getUTCNow(), "phase", null);
-        allTransitions.add(tr3);
-
-        effectiveDate = effectiveDate.plusDays(15);
-        clock.addDays(15);
-        final SubscriptionBaseTransition tr4 = createTransition(entitlementId, EventType.API_USER, ApiEventType.RE_CREATE, requestedDate, effectiveDate, clock.getUTCNow(), null, "phase");
-        allTransitions.add(tr4);
-
-        final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
-        entitlements.add(entitlement);
-
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, Collections.<BlockingState>emptyList());
-
-        assertEquals(timeline.getAccountId(), accountId);
-        assertEquals(timeline.getBundleId(), bundleId);
-        assertEquals(timeline.getExternalKey(), externalKey);
-
-        final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
-        assertEquals(events.size(), 7);
-
-        assertEquals(events.get(0).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(1).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(2).getEffectiveDate().compareTo(new LocalDate(tr2.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(3).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(4).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(5).getEffectiveDate().compareTo(new LocalDate(tr4.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(6).getEffectiveDate().compareTo(new LocalDate(tr4.getEffectiveTransitionTime(), accountTimeZone)), 0);
-
-        assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
-        assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
-        assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.PHASE);
-        assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.PAUSE_ENTITLEMENT);
-        assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.PAUSE_BILLING);
-        assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.RESUME_ENTITLEMENT);
-        assertEquals(events.get(6).getSubscriptionEventType(), SubscriptionEventType.RESUME_BILLING);
-
-        assertEquals(events.get(0).getNextPhase().getName(), "trial");
-        assertEquals(events.get(1).getNextPhase().getName(), "trial");
-        assertEquals(events.get(2).getNextPhase().getName(), "phase");
-        assertEquals(events.get(3).getNextPhase(), null);
-        assertEquals(events.get(4).getNextPhase(), null);
-        assertEquals(events.get(5).getNextPhase().getName(), "phase");
-        assertEquals(events.get(6).getNextPhase().getName(), "phase");
+        assertEquals(events.get(3).getPrevPhase().getName(), "phase");
+        assertNull(events.get(3).getNextPhase());
     }
 
     @Test(groups = "fast")
@@ -455,11 +389,16 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
         assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
+        assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
+        assertNull(events.get(1).getPrevPhase());
         assertEquals(events.get(1).getNextPhase().getName(), "trial");
+        assertEquals(events.get(2).getPrevPhase().getName(), "trial");
         assertEquals(events.get(2).getNextPhase().getName(), "phase");
+        assertEquals(events.get(3).getPrevPhase().getName(), "phase");
         assertEquals(events.get(3).getNextPhase().getName(), "phase");
-        assertEquals(events.get(4).getNextPhase(), null);
+        assertEquals(events.get(4).getPrevPhase().getName(), "phase");
+        assertNull(events.get(4).getNextPhase());
     }
 
     @Test(groups = "fast")
@@ -526,18 +465,21 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
         assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
-        assertEquals(events.get(0).getPrevPhase(), null);
+        assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
-        assertEquals(events.get(1).getPrevPhase(), null);
+        assertNull(events.get(1).getPrevPhase());
         assertEquals(events.get(1).getNextPhase().getName(), "trial");
+
         assertEquals(events.get(2).getPrevPhase().getName(), "trial");
         assertEquals(events.get(2).getNextPhase().getName(), "phase");
+
         assertEquals(events.get(3).getPrevPhase().getName(), "phase");
         assertEquals(events.get(3).getNextPhase().getName(), "phase");
+
         assertEquals(events.get(4).getPrevPhase().getName(), "phase");
-        assertEquals(events.get(4).getNextPhase(), null);
+        assertNull(events.get(4).getNextPhase());
         assertEquals(events.get(5).getPrevPhase().getName(), "phase");
-        assertEquals(events.get(5).getNextPhase(), null);
+        assertNull(events.get(5).getNextPhase());
     }
 
     @Test(groups = "fast")
@@ -626,13 +568,14 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(7).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
         assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
-        assertEquals(events.get(0).getPrevPhase(), null);
+        assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial1");
-        assertEquals(events.get(1).getPrevPhase(), null);
+        assertNull(events.get(1).getPrevPhase());
         assertEquals(events.get(1).getNextPhase().getName(), "trial1");
-        assertEquals(events.get(2).getPrevPhase(), null);
+
+        assertNull(events.get(2).getPrevPhase());
         assertEquals(events.get(2).getNextPhase().getName(), "phase2");
-        assertEquals(events.get(3).getPrevPhase(), null);
+        assertNull(events.get(3).getPrevPhase());
         assertEquals(events.get(3).getNextPhase().getName(), "phase2");
 
         assertEquals(events.get(4).getPrevPhase().getName(), "trial1");
@@ -642,17 +585,17 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(5).getNextPhase().getName(), "phase1");
 
         assertEquals(events.get(6).getPrevPhase().getName(), "phase2");
-        assertEquals(events.get(6).getNextPhase(), null);
+        assertEquals(events.get(6).getNextPhase().getName(), "phase2");
 
         assertEquals(events.get(7).getPrevPhase().getName(), "phase1");
-        assertEquals(events.get(7).getNextPhase(), null);
+        assertNull(events.get(7).getNextPhase());
 
         assertEquals(events.get(8).getPrevPhase().getName(), "phase1");
-        assertEquals(events.get(8).getNextPhase(), null);
+        assertNull(events.get(8).getNextPhase());
     }
 
     @Test(groups = "fast")
-    public void testWithOverdueOfflineAndClear() throws CatalogApiException {
+    public void testWithOverdueOffline() throws CatalogApiException {
         clock.setDay(new LocalDate(2013, 1, 1));
 
         final DateTimeZone accountTimeZone = DateTimeZone.UTC;
@@ -676,21 +619,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         final SubscriptionBaseTransition tr2 = createTransition(entitlementId, EventType.PHASE, null, requestedDate, effectiveDate, clock.getUTCNow(), "trial", "phase");
         allTransitions.add(tr2);
 
-        effectiveDate = effectiveDate.plusDays(6); // 2013-02-06
-        clock.addDays(6);
+        effectiveDate = effectiveDate.plusDays(40); // 2013-03-12
+        clock.addDays(40);
         final SubscriptionBaseTransition tr3 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CANCEL, requestedDate, effectiveDate, clock.getUTCNow(), "phase", null);
         allTransitions.add(tr3);
 
-        effectiveDate = effectiveDate.plusDays(22);// 2013-02-28
-        clock.addDays(22);
-        final SubscriptionBaseTransition tr4 = createTransition(entitlementId, EventType.API_USER, ApiEventType.RE_CREATE, requestedDate, effectiveDate, clock.getUTCNow(), null, "phase");
-        allTransitions.add(tr4);
-
-        effectiveDate = effectiveDate.plusDays(12); // 2013-03-12
-        clock.addDays(12);
-        final SubscriptionBaseTransition tr5 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CANCEL, requestedDate, effectiveDate, clock.getUTCNow(), "phase", null);
-        allTransitions.add(tr5);
-
         final BlockingState bs1 = new DefaultBlockingState(UUID.randomUUID(), entitlementId, BlockingStateType.ACCOUNT,
                                                            "OFFLINE", "overdue-service",
                                                            true, true, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
@@ -703,15 +636,6 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
         blockingStates.add(bs2);
 
-        effectiveDate = effectiveDate.plusDays(12); // 2013-03-24
-        clock.addDays(12);
-
-        final BlockingState bs3 = new DefaultBlockingState(UUID.randomUUID(), entitlementId, BlockingStateType.ACCOUNT,
-                                                           "__KILLBILL__CLEAR__OVERDUE__STATE__", "overdue-service",
-                                                           false, false, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
-
-        blockingStates.add(bs3);
-
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
         final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
         entitlements.add(entitlement);
@@ -723,43 +647,117 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(timeline.getExternalKey(), externalKey);
 
         final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
-        assertEquals(events.size(), 11);
+        assertEquals(events.size(), 6);
 
         assertEquals(events.get(0).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
         assertEquals(events.get(1).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
         assertEquals(events.get(2).getEffectiveDate().compareTo(new LocalDate(tr2.getEffectiveTransitionTime(), accountTimeZone)), 0);
         assertEquals(events.get(3).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
         assertEquals(events.get(4).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(5).getEffectiveDate().compareTo(new LocalDate(tr4.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(6).getEffectiveDate().compareTo(new LocalDate(tr4.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(7).getEffectiveDate().compareTo(new LocalDate(tr5.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(8).getEffectiveDate().compareTo(new LocalDate(tr5.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(9).getEffectiveDate().compareTo(new LocalDate(tr5.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(10).getEffectiveDate().compareTo(new LocalDate(bs3.getEffectiveDate(), accountTimeZone)), 0);
+        assertEquals(events.get(5).getEffectiveDate().compareTo(new LocalDate(bs1.getEffectiveDate(), accountTimeZone)), 0);
 
         assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
         assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.PHASE);
-        assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.PAUSE_ENTITLEMENT);
-        assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.PAUSE_BILLING);
-        assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.RESUME_ENTITLEMENT);
-        assertEquals(events.get(6).getSubscriptionEventType(), SubscriptionEventType.RESUME_BILLING);
-        assertEquals(events.get(7).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
-        assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
-        assertEquals(events.get(9).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
-        assertEquals(events.get(10).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
+        assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+        assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
+        assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
 
+        assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
+        assertNull(events.get(1).getPrevPhase());
         assertEquals(events.get(1).getNextPhase().getName(), "trial");
+
+        assertEquals(events.get(2).getPrevPhase().getName(), "trial");
         assertEquals(events.get(2).getNextPhase().getName(), "phase");
-        assertEquals(events.get(3).getNextPhase(), null);
-        assertEquals(events.get(4).getNextPhase(), null);
-        assertEquals(events.get(5).getNextPhase().getName(), "phase");
-        assertEquals(events.get(6).getNextPhase().getName(), "phase");
-        assertEquals(events.get(7).getNextPhase(), null);
-        assertEquals(events.get(8).getNextPhase(), null);
-        assertEquals(events.get(9).getNextPhase(), null);
-        assertEquals(events.get(10).getNextPhase(), null);
+
+        assertEquals(events.get(3).getPrevPhase().getName(), "phase");
+        assertNull(events.get(3).getNextPhase());
+        assertEquals(events.get(4).getPrevPhase().getName(), "phase");
+        assertNull(events.get(4).getNextPhase());
+
+        assertEquals(events.get(5).getPrevPhase().getName(), "phase");
+        assertNull(events.get(5).getNextPhase());
+    }
+
+    @Test(groups = "fast", description = "Test for https://github.com/killbill/killbill/issues/134")
+    public void testRemoveOverlappingBlockingStates() throws CatalogApiException {
+        clock.setDay(new LocalDate(2013, 1, 1));
+
+        final DateTimeZone accountTimeZone = DateTimeZone.UTC;
+        final UUID accountId = UUID.randomUUID();
+        final UUID bundleId = UUID.randomUUID();
+        final String externalKey = "foo";
+
+        final UUID entitlementId = UUID.randomUUID();
+
+        final List<SubscriptionBaseTransition> allTransitions = new ArrayList<SubscriptionBaseTransition>();
+        final List<BlockingState> blockingStates = new ArrayList<BlockingState>();
+
+        final DateTime requestedDate = new DateTime();
+        DateTime effectiveDate = new DateTime(2013, 1, 1, 15, 43, 25, 0, DateTimeZone.UTC);
+        final SubscriptionBaseTransition tr1 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CREATE, requestedDate, effectiveDate, clock.getUTCNow(), null, "trial");
+        allTransitions.add(tr1);
+
+        // Overlapping ENT_STATE_BLOCKED - should merge
+        effectiveDate = effectiveDate.plusDays(5);
+        clock.addDays(5);
+        final BlockingState bs1 = new DefaultBlockingState(UUID.randomUUID(), entitlementId, BlockingStateType.SUBSCRIPTION,
+                                                           DefaultEntitlementApi.ENT_STATE_BLOCKED, DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                           true, true, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+        blockingStates.add(bs1);
+
+        effectiveDate = effectiveDate.plusDays(1);
+        clock.addDays(1);
+        final BlockingState bs2 = new DefaultBlockingState(UUID.randomUUID(), bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE,
+                                                           DefaultEntitlementApi.ENT_STATE_BLOCKED, DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                           true, true, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+        blockingStates.add(bs2);
+
+        // Overlapping ENT_STATE_CANCELLED - should merge
+        effectiveDate = effectiveDate.plusDays(1);
+        clock.addDays(1);
+        final BlockingState bs3 = new DefaultBlockingState(UUID.randomUUID(), accountId, BlockingStateType.ACCOUNT,
+                                                           DefaultEntitlementApi.ENT_STATE_CANCELLED, DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                           true, true, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+        blockingStates.add(bs3);
+        final BlockingState bs4 = new DefaultBlockingState(UUID.randomUUID(), bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE,
+                                                           DefaultEntitlementApi.ENT_STATE_CANCELLED, DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                           true, true, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+        blockingStates.add(bs4);
+
+        final List<Entitlement> entitlements = new ArrayList<Entitlement>();
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        entitlements.add(entitlement);
+
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+
+        final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
+        assertEquals(events.size(), 4);
+
+        assertEquals(events.get(0).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
+        assertEquals(events.get(1).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
+        assertEquals(events.get(2).getEffectiveDate().compareTo(new LocalDate(bs1.getEffectiveDate(), accountTimeZone)), 0);
+        assertEquals(events.get(3).getEffectiveDate().compareTo(new LocalDate(bs3.getEffectiveDate(), accountTimeZone)), 0);
+
+        assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+        assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+        assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.PAUSE_ENTITLEMENT);
+        assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+
+        assertNull(events.get(0).getPrevPhase());
+        assertEquals(events.get(0).getNextPhase().getName(), "trial");
+        assertNull(events.get(1).getPrevPhase());
+        assertEquals(events.get(1).getNextPhase().getName(), "trial");
+
+        assertEquals(events.get(2).getPrevPhase().getName(), "trial");
+        assertEquals(events.get(2).getNextPhase().getName(), "trial");
+
+        assertEquals(events.get(3).getPrevPhase().getName(), "trial");
+        assertNull(events.get(3).getNextPhase());
     }
 
     private Entitlement createEntitlement(final UUID entitlementId, final List<SubscriptionBaseTransition> allTransitions) {
@@ -781,25 +779,53 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
                                                         final String prevPhaseName,
                                                         final String nextPhaseName
                                                        ) throws CatalogApiException {
-
-        final PlanPhase prevPhase = prevPhaseName != null ? Mockito.mock(PlanPhase.class) : null;
-        if (prevPhase != null) {
+        final PlanPhase prevPhase;
+        final Plan prevPlan;
+        final Product prevProduct;
+        final PriceList prevPriceList;
+        if (prevPhaseName == null) {
+            prevPhase = null;
+            prevPlan = null;
+            prevProduct = null;
+            prevPriceList = null;
+        } else {
+            prevPhase = Mockito.mock(PlanPhase.class);
             Mockito.when(prevPhase.getName()).thenReturn(prevPhaseName);
+
+            prevProduct = Mockito.mock(Product.class);
+            Mockito.when(prevProduct.getName()).thenReturn("product");
+
+            prevPlan = Mockito.mock(Plan.class);
+            Mockito.when(prevPlan.getName()).thenReturn("plan");
+            Mockito.when(prevPlan.getProduct()).thenReturn(prevProduct);
+
+            prevPriceList = Mockito.mock(PriceList.class);
+            Mockito.when(prevPriceList.getName()).thenReturn("pricelist");
         }
 
-        final PlanPhase nextPhase = nextPhaseName != null ? Mockito.mock(PlanPhase.class) : null;
-        if (nextPhase != null) {
+        final PlanPhase nextPhase;
+        final Plan nextPlan;
+        final Product nextProduct;
+        final PriceList nextPriceList;
+        if (nextPhaseName == null) {
+            nextPhase = null;
+            nextPlan = null;
+            nextProduct = null;
+            nextPriceList = null;
+        } else {
+            nextPhase = Mockito.mock(PlanPhase.class);
             Mockito.when(nextPhase.getName()).thenReturn(nextPhaseName);
-        }
 
-        final Plan plan = Mockito.mock(Plan.class);
-        Mockito.when(plan.getName()).thenReturn("plan");
+            nextProduct = Mockito.mock(Product.class);
+            Mockito.when(nextProduct.getName()).thenReturn("product");
 
-        final Product product = Mockito.mock(Product.class);
-        Mockito.when(product.getName()).thenReturn("product");
+            nextPlan = Mockito.mock(Plan.class);
+            Mockito.when(nextPlan.getName()).thenReturn("plan");
+            Mockito.when(nextPlan.getProduct()).thenReturn(nextProduct);
 
-        final PriceList priceList = Mockito.mock(PriceList.class);
-        Mockito.when(priceList.getName()).thenReturn("pricelist");
+            nextPriceList = Mockito.mock(PriceList.class);
+            Mockito.when(nextPriceList.getName()).thenReturn("pricelist");
+        }
 
         return new SubscriptionBaseTransitionData(UUID.randomUUID(),
                                                   entitlementId,
@@ -811,15 +837,15 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
                                                   null,
                                                   null,
                                                   null,
-                                                  plan,
+                                                  prevPlan,
                                                   prevPhase,
-                                                  priceList,
+                                                  prevPriceList,
                                                   null,
                                                   null,
                                                   null,
-                                                  plan,
+                                                  nextPlan,
                                                   nextPhase,
-                                                  priceList,
+                                                  nextPriceList,
                                                   1L,
                                                   createdDate,
                                                   UUID.randomUUID(),