killbill-memoizeit

entitlement: export events at the subscription level The

2/10/2015 5:33:58 PM

Details

diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java
new file mode 100644
index 0000000..18f9399
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.entitlement.api;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.entitlement.DefaultEntitlementService;
+import org.killbill.billing.entitlement.block.BlockingChecker.BlockingAggregator;
+import org.killbill.billing.entitlement.block.DefaultBlockingChecker.DefaultBlockingAggregator;
+import org.killbill.billing.junction.DefaultBlockingState;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+// Given an event stream (across one or multiple entitlements), insert the blocking events at the right place
+public class BlockingStateOrdering extends EntitlementOrderingBase {
+
+    private static final BlockingStateOrdering INSTANCE = new BlockingStateOrdering();
+
+    private BlockingStateOrdering() {}
+
+    public static void insertSorted(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone, final LinkedList<SubscriptionEvent> result) {
+        INSTANCE.computeEvents(entitlements, accountTimeZone, result);
+    }
+
+    private void computeEvents(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone, final LinkedList<SubscriptionEvent> result) {
+        final Collection<UUID> allEntitlementUUIDs = new HashSet<UUID>();
+        final Collection<BlockingState> blockingStates = new LinkedList<BlockingState>();
+        for (final Entitlement entitlement : entitlements) {
+            allEntitlementUUIDs.add(entitlement.getId());
+            Preconditions.checkState(entitlement instanceof DefaultEntitlement, "Entitlement %s is not a DefaultEntitlement", entitlement);
+            blockingStates.addAll(((DefaultEntitlement) entitlement).getEventsStream().getBlockingStates());
+        }
+
+        // Trust the incoming ordering here: blocking states were sorted using ProxyBlockingStateDao#sortedCopy
+        for (final BlockingState bs : blockingStates) {
+            final List<SubscriptionEvent> newEvents = new ArrayList<SubscriptionEvent>();
+            final int index = insertFromBlockingEvent(accountTimeZone, allEntitlementUUIDs, result, bs, bs.getEffectiveDate(), newEvents);
+            insertAfterIndex(result, newEvents, index);
+        }
+    }
+
+    // Returns the index and the newEvents generated from the incoming blocking state event. Those new events will all be created for the same effectiveDate and should be ordered.
+    private int insertFromBlockingEvent(final DateTimeZone accountTimeZone, final Collection<UUID> allEntitlementUUIDs, final List<SubscriptionEvent> result, final BlockingState bs, final DateTime bsEffectiveDate, final List<SubscriptionEvent> newEvents) {
+        // Keep the current state per entitlement
+        final Map<UUID, TargetState> targetStates = new HashMap<UUID, TargetState>();
+        for (final UUID cur : allEntitlementUUIDs) {
+            targetStates.put(cur, new TargetState());
+        }
+
+        //
+        // Find out where to insert next event, and calculate current state for each entitlement at the position where we stop.
+        //
+        int index = -1;
+        final Iterator<SubscriptionEvent> it = result.iterator();
+        // Where we need to insert in that stream
+        DefaultSubscriptionEvent curInsertion = null;
+        while (it.hasNext()) {
+            final DefaultSubscriptionEvent cur = (DefaultSubscriptionEvent) it.next();
+            final int compEffectiveDate = bsEffectiveDate.compareTo(cur.getEffectiveDateTime());
+            final boolean shouldContinue = (compEffectiveDate >= 0);
+            if (!shouldContinue) {
+                break;
+            }
+            index++;
+
+            final TargetState curTargetState = targetStates.get(cur.getEntitlementId());
+            switch (cur.getSubscriptionEventType()) {
+                case START_ENTITLEMENT:
+                    curTargetState.setEntitlementStarted();
+                    break;
+                case STOP_ENTITLEMENT:
+                    curTargetState.setEntitlementStopped();
+                    break;
+                case START_BILLING:
+                    curTargetState.setBillingStarted();
+                    break;
+                case PAUSE_BILLING:
+                case PAUSE_ENTITLEMENT:
+                case RESUME_ENTITLEMENT:
+                case RESUME_BILLING:
+                case SERVICE_STATE_CHANGE:
+                    curTargetState.addEntitlementEvent(cur);
+                    break;
+                case STOP_BILLING:
+                    curTargetState.setBillingStopped();
+                    break;
+            }
+            curInsertion = cur;
+        }
+
+        // Extract the list of targets based on the type of blocking state
+        final List<UUID> targetEntitlementIds = bs.getType() == BlockingStateType.SUBSCRIPTION ? ImmutableList.<UUID>of(bs.getBlockedId()) :
+                                                ImmutableList.<UUID>copyOf(allEntitlementUUIDs);
+
+        // For each target compute the new events that should be inserted in the stream
+        for (final UUID targetEntitlementId : targetEntitlementIds) {
+            final SubscriptionEvent[] prevNext = findPrevNext(result, targetEntitlementId, curInsertion);
+            final TargetState curTargetState = targetStates.get(targetEntitlementId);
+
+            final List<SubscriptionEventType> eventTypes = curTargetState.addStateAndReturnEventTypes(bs);
+            for (final SubscriptionEventType t : eventTypes) {
+                newEvents.add(toSubscriptionEvent(prevNext[0], prevNext[1], targetEntitlementId, bs, t, accountTimeZone));
+            }
+        }
+        return index;
+    }
+
+    // Extract prev and next events in the stream events for that particular target subscription from the insertionEvent
+    private SubscriptionEvent[] findPrevNext(final List<SubscriptionEvent> events, final UUID targetEntitlementId, final SubscriptionEvent insertionEvent) {
+        // Find prev/next event for the same entitlement
+        final SubscriptionEvent[] result = new DefaultSubscriptionEvent[2];
+        if (insertionEvent == null) {
+            result[0] = null;
+            result[1] = !events.isEmpty() ? events.get(0) : null;
+            return result;
+        }
+
+        final Iterator<SubscriptionEvent> it = events.iterator();
+        DefaultSubscriptionEvent prev = null;
+        DefaultSubscriptionEvent next = null;
+        boolean foundCur = false;
+        while (it.hasNext()) {
+            final DefaultSubscriptionEvent tmp = (DefaultSubscriptionEvent) it.next();
+            if (tmp.getEntitlementId().equals(targetEntitlementId)) {
+                if (!foundCur) {
+                    prev = tmp;
+                } else {
+                    next = tmp;
+                    break;
+                }
+            }
+            // Check both the id and the event type because of multiplexing
+            if (tmp.getId().equals(insertionEvent.getId()) &&
+                tmp.getSubscriptionEventType().equals(insertionEvent.getSubscriptionEventType())) {
+                foundCur = true;
+            }
+        }
+        result[0] = prev;
+        result[1] = next;
+        return result;
+    }
+
+    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();
+        }
+
+        // See https://github.com/killbill/killbill/issues/135
+        final String serviceName = getRealServiceNameForEntitlementOrExternalServiceName(in.getService(), eventType);
+
+        return new DefaultSubscriptionEvent(in.getId(),
+                                            entitlementId,
+                                            in.getEffectiveDate(),
+                                            in.getCreatedDate(),
+                                            eventType,
+                                            in.isBlockEntitlement(),
+                                            in.isBlockBilling(),
+                                            serviceName,
+                                            in.getStateName(),
+                                            prevProduct,
+                                            prevPlan,
+                                            prevPlanPhase,
+                                            prevPriceList,
+                                            prevBillingPeriod,
+                                            nextProduct,
+                                            nextPlan,
+                                            nextPlanPhase,
+                                            nextPriceList,
+                                            nextBillingPeriod,
+                                            in.getCreatedDate(),
+                                            accountTimeZone);
+    }
+
+    private void insertAfterIndex(final LinkedList<SubscriptionEvent> original, final Collection<SubscriptionEvent> newEvents, final int index) {
+        final boolean firstPosition = (index == -1);
+        final boolean lastPosition = (index == original.size() - 1);
+        if (lastPosition || firstPosition) {
+            for (final SubscriptionEvent cur : newEvents) {
+                if (lastPosition) {
+                    original.addLast(cur);
+                } else {
+                    original.addFirst(cur);
+                }
+            }
+        } else {
+            original.addAll(index + 1, newEvents);
+        }
+    }
+
+    //
+    // Internal class to keep the state associated with each subscription
+    //
+    private static final class TargetState {
+
+        private final Map<String, BlockingState> perServiceBlockingState;
+
+        private boolean isEntitlementStarted;
+        private boolean isEntitlementStopped;
+        private boolean isBillingStarted;
+        private boolean isBillingStopped;
+
+        public TargetState() {
+            this.isEntitlementStarted = false;
+            this.isEntitlementStopped = false;
+            this.isBillingStarted = false;
+            this.isBillingStopped = false;
+            this.perServiceBlockingState = new HashMap<String, BlockingState>();
+        }
+
+        public void setEntitlementStarted() {
+            isEntitlementStarted = true;
+        }
+
+        public void setEntitlementStopped() {
+            isEntitlementStopped = true;
+        }
+
+        public void setBillingStarted() {
+            isBillingStarted = true;
+        }
+
+        public void setBillingStopped() {
+            isBillingStopped = true;
+        }
+
+        public void addEntitlementEvent(final SubscriptionEvent e) {
+            final String serviceName = getRealServiceNameForEntitlementOrExternalServiceName(e.getServiceName(), e.getSubscriptionEventType());
+            final BlockingState lastBlockingStateForService = perServiceBlockingState.get(serviceName);
+
+            // Assume the event has no impact on changes - TODO this is wrong for SERVICE_STATE_CHANGE
+            final boolean blockChange = lastBlockingStateForService != null && lastBlockingStateForService.isBlockChange();
+            // For block entitlement or billing, override the previous state
+            final boolean blockedEntitlement = e.isBlockedEntitlement();
+            final boolean blockedBilling = e.isBlockedBilling();
+
+            final BlockingState converted = new DefaultBlockingState(e.getEntitlementId(),
+                                                                     BlockingStateType.SUBSCRIPTION,
+                                                                     e.getServiceStateName(),
+                                                                     serviceName,
+                                                                     blockChange,
+                                                                     blockedEntitlement,
+                                                                     blockedBilling,
+                                                                     ((DefaultSubscriptionEvent) e).getEffectiveDateTime());
+            perServiceBlockingState.put(converted.getService(), converted);
+        }
+
+        //
+        // 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(),
+                                                                              bs.getStateName(),
+                                                                              bs.getService(),
+                                                                              bs.isBlockChange(),
+                                                                              (bs.isBlockEntitlement() && isEntitlementStarted && !isEntitlementStopped),
+                                                                              (bs.isBlockBilling() && isBillingStarted && !isBillingStopped),
+                                                                              bs.getEffectiveDate());
+
+            final List<SubscriptionEventType> result = new ArrayList<SubscriptionEventType>(4);
+            if (fixedBlockingState.getStateName().equals(DefaultEntitlementApi.ENT_STATE_CANCELLED)) {
+                isEntitlementStopped = true;
+                result.add(SubscriptionEventType.STOP_ENTITLEMENT);
+                return result;
+            }
+
+            //
+            // We look at the effect of the incoming event for the specific service, and then recompute the state after so we can compare if anything has changed
+            // across all services
+            //
+            final BlockingAggregator stateBefore = getState();
+            if (DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(fixedBlockingState.getService())) {
+                // Some blocking states will be added as entitlement-service and billing-service via addEntitlementEvent
+                // (see above). Because of it, we need to multiplex entitlement events here.
+                // TODO - this is magic and fragile. We should revisit how we create this state machine.
+                perServiceBlockingState.put(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME, fixedBlockingState);
+                perServiceBlockingState.put(BILLING_SERVICE_NAME, fixedBlockingState);
+            } else {
+                perServiceBlockingState.put(fixedBlockingState.getService(), fixedBlockingState);
+            }
+            final BlockingAggregator stateAfter = getState();
+
+            final boolean shouldResumeEntitlement = isEntitlementStarted && !isEntitlementStopped && stateBefore.isBlockEntitlement() && !stateAfter.isBlockEntitlement();
+            if (shouldResumeEntitlement) {
+                result.add(SubscriptionEventType.RESUME_ENTITLEMENT);
+            }
+            final boolean shouldResumeBilling = isBillingStarted && !isBillingStopped && stateBefore.isBlockBilling() && !stateAfter.isBlockBilling();
+            if (shouldResumeBilling) {
+                result.add(SubscriptionEventType.RESUME_BILLING);
+            }
+
+            final boolean shouldBlockEntitlement = isEntitlementStarted && !isEntitlementStopped && !stateBefore.isBlockEntitlement() && stateAfter.isBlockEntitlement();
+            if (shouldBlockEntitlement) {
+                result.add(SubscriptionEventType.PAUSE_ENTITLEMENT);
+            }
+            final boolean shouldBlockBilling = isBillingStarted && !isBillingStopped && !stateBefore.isBlockBilling() && stateAfter.isBlockBilling();
+            if (shouldBlockBilling) {
+                result.add(SubscriptionEventType.PAUSE_BILLING);
+            }
+
+            if (!shouldResumeEntitlement && !shouldResumeBilling && !shouldBlockEntitlement && !shouldBlockBilling && !fixedBlockingState.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
+                result.add(SubscriptionEventType.SERVICE_STATE_CHANGE);
+            }
+            return result;
+        }
+
+        private BlockingAggregator getState() {
+            final DefaultBlockingAggregator aggrBefore = new DefaultBlockingAggregator();
+            for (final BlockingState cur : perServiceBlockingState.values()) {
+                aggrBefore.or(cur);
+            }
+            return aggrBefore;
+        }
+    }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java
index e422e72..57fc554 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscription.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -17,6 +19,7 @@
 package org.killbill.billing.entitlement.api;
 
 import java.util.Collection;
+import java.util.List;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
@@ -78,4 +81,9 @@ public class DefaultSubscription extends DefaultEntitlement implements Subscript
             return blockingState == null ? null : blockingState.getService();
         }
     }
+
+    @Override
+    public List<SubscriptionEvent> getSubscriptionEvents() {
+        return SubscriptionEventOrdering.sortedCopy(this, getAccountTimeZone());
+    }
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
index ecad08e..91e5758 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
@@ -18,588 +18,23 @@
 
 package org.killbill.billing.entitlement.api;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
-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.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.Plan;
-import org.killbill.billing.catalog.api.PlanPhase;
-import org.killbill.billing.catalog.api.PriceList;
-import org.killbill.billing.catalog.api.Product;
-import org.killbill.billing.entitlement.DefaultEntitlementService;
-import org.killbill.billing.entitlement.block.BlockingChecker.BlockingAggregator;
-import org.killbill.billing.entitlement.block.DefaultBlockingChecker.DefaultBlockingAggregator;
-import org.killbill.billing.junction.DefaultBlockingState;
-import org.killbill.billing.subscription.api.SubscriptionBase;
-import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
-import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
 
 public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTimeline {
 
-    private final Logger logger = LoggerFactory.getLogger(DefaultSubscriptionBundleTimeline.class);
-
-    public static final String BILLING_SERVICE_NAME = "billing-service";
-    public static final String ENT_BILLING_SERVICE_NAME = "entitlement+billing-service";
-
-    private final List<SubscriptionEvent> events;
     private final UUID accountId;
     private final UUID bundleId;
     private final String externalKey;
+    private final List<SubscriptionEvent> events;
 
-    public DefaultSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final Collection<Entitlement> entitlements) {
-        // Trust the incoming ordering here: blocking states were sorted using ProxyBlockingStateDao#sortedCopy
-        final List<BlockingState> blockingStates = new LinkedList<BlockingState>();
-        for (final Entitlement entitlement : entitlements) {
-            blockingStates.addAll(((DefaultEntitlement) entitlement).getEventsStream().getBlockingStates());
-        }
-        this.accountId = accountId;
-        this.bundleId = bundleId;
-        this.externalKey = externalKey;
-        this.events = computeEvents(entitlements, blockingStates, accountTimeZone);
-    }
-
-    @VisibleForTesting
-    DefaultSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final Collection<Entitlement> entitlements, final List<BlockingState> allBlockingStates) {
+    public DefaultSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final Iterable<Entitlement> entitlements) {
         this.accountId = accountId;
         this.bundleId = bundleId;
         this.externalKey = externalKey;
-        this.events = computeEvents(entitlements, allBlockingStates, accountTimeZone);
-    }
-
-    //
-    // Compute all events based on blocking states events and base subscription events
-    // Note that:
-    // - base subscription events are already ordered for each Entitlement and so when we reorder at the bundle level we try not to break that initial ordering
-    // - 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<UUID>(Collections2.transform(entitlements, new Function<Entitlement, UUID>() {
-            @Override
-            public UUID apply(final Entitlement input) {
-                return input.getId();
-            }
-        }));
-
-        // Compute base events across all entitlements
-        final LinkedList<SubscriptionEvent> result = computeSubscriptionBaseEvents(entitlements, accountTimeZone);
-
-        for (final BlockingState bs : allBlockingStates) {
-            final List<SubscriptionEvent> newEvents = new ArrayList<SubscriptionEvent>();
-            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.
-    //
-    // All this seems a little over complicated, and one wonders why we don't just shove all events and call Collections.sort on the list prior
-    // to return:
-    // - One explanation is that we don't know the events in advance and each time the new events to be inserted are computed from the current state
-    //   of the stream, which requires ordering all along
-    // - A careful reader will notice that the algorithm is N^2, -- so that we care so much considering we have very events -- but in addition to that
-    //   the recursive path will be used very infrequently and when it is used, this will be probably just reorder with the prev event and that's it.
-    //
-    @VisibleForTesting
-    protected void reOrderSubscriptionEventsOnSameDateByType(final List<SubscriptionEvent> events) {
-        final int size = events.size();
-        for (int i = 0; i < size; i++) {
-            final SubscriptionEvent cur = events.get(i);
-            final SubscriptionEvent next = (i < (size - 1)) ? events.get(i + 1) : null;
-
-            final boolean shouldSwap = (next != null && shouldSwap(cur, next, true));
-            final boolean shouldReverseSort = (next == null || shouldSwap);
-
-            int currentIndex = i;
-            if (shouldSwap) {
-                Collections.swap(events, i, i + 1);
-            }
-            if (shouldReverseSort) {
-                while (currentIndex >= 1) {
-                    final SubscriptionEvent revCur = events.get(currentIndex);
-                    final SubscriptionEvent other = events.get(currentIndex - 1);
-                    if (shouldSwap(revCur, other, false)) {
-                        Collections.swap(events, currentIndex, currentIndex - 1);
-                    }
-                    if (revCur.getEffectiveDate().compareTo(other.getEffectiveDate()) != 0) {
-                        break;
-                    }
-                    currentIndex--;
-                }
-            }
-        }
-    }
-
-    private Integer compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(final SubscriptionEvent first, final SubscriptionEvent second) {
-        // For consistency, make sure entitlement-service and billing-service events always happen in a
-        // deterministic order (e.g. after other services for STOP events and before for START events)
-        if ((DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(first.getServiceName()) ||
-             DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME.equals(first.getServiceName())) &&
-            !(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(second.getServiceName()) ||
-              DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME.equals(second.getServiceName()))) {
-            // first is an entitlement-service or billing-service event, but not second
-            if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT) ||
-                first.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING) ||
-                first.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_ENTITLEMENT) ||
-                first.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_BILLING) ||
-                first.getSubscriptionEventType().equals(SubscriptionEventType.PHASE) ||
-                first.getSubscriptionEventType().equals(SubscriptionEventType.CHANGE)) {
-                return -1;
-            } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_ENTITLEMENT) ||
-                       first.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_BILLING) ||
-                       first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT) ||
-                       first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
-                return 1;
-            } else {
-                // Default behavior
-                return -1;
-            }
-        } else if ((DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(second.getServiceName()) ||
-                    DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME.equals(second.getServiceName())) &&
-                   !(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(first.getServiceName()) ||
-                     DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME.equals(first.getServiceName()))) {
-            // second is an entitlement-service or billing-service event, but not first
-            if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT) ||
-                second.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING) ||
-                second.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_ENTITLEMENT) ||
-                second.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_BILLING) ||
-                second.getSubscriptionEventType().equals(SubscriptionEventType.PHASE) ||
-                second.getSubscriptionEventType().equals(SubscriptionEventType.CHANGE)) {
-                return 1;
-            } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_ENTITLEMENT) ||
-                       second.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_BILLING) ||
-                       second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT) ||
-                       second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
-                return -1;
-            } else {
-                // Default behavior
-                return 1;
-            }
-        } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT)) {
-            // START_ENTITLEMENT is always first
-            return -1;
-        } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT)) {
-            // START_ENTITLEMENT is always first
-            return 1;
-        } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
-            // STOP_BILLING is always last
-            return 1;
-        } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
-            // STOP_BILLING is always last
-            return -1;
-        } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING)) {
-            // START_BILLING is first after START_ENTITLEMENT
-            return -1;
-        } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING)) {
-            // START_BILLING is first after START_ENTITLEMENT
-            return 1;
-        } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT)) {
-            // STOP_ENTITLEMENT is last after STOP_BILLING
-            return 1;
-        } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT)) {
-            // STOP_ENTITLEMENT is last after STOP_BILLING
-            return -1;
-        } else {
-            // Trust the current ordering
-            return null;
-        }
-    }
-
-    private boolean shouldSwap(final SubscriptionEvent cur, final SubscriptionEvent other, final boolean isAscending) {
-        // For a given date, order by subscriptionId, and within subscription by event type
-        final int idComp = cur.getEntitlementId().compareTo(other.getEntitlementId());
-        final Integer comparison = compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(cur, other);
-        return (cur.getEffectiveDate().compareTo(other.getEffectiveDate()) == 0 &&
-                ((isAscending &&
-                  ((idComp > 0) ||
-                   (idComp == 0 && comparison != null && comparison > 0))) ||
-                 (!isAscending &&
-                  ((idComp < 0) ||
-                   (idComp == 0 && comparison != null && comparison < 0)))));
-    }
-
-    private void insertAfterIndex(final LinkedList<SubscriptionEvent> original, final List<SubscriptionEvent> newEvents, final int index) {
-        final boolean firstPosition = (index == -1);
-        final boolean lastPosition = (index == original.size() - 1);
-        if (lastPosition || firstPosition) {
-            for (final SubscriptionEvent cur : newEvents) {
-                if (lastPosition) {
-                    original.addLast(cur);
-                } else {
-                    original.addFirst(cur);
-                }
-            }
-        } else {
-            original.addAll(index + 1, newEvents);
-        }
-    }
-
-    //
-    // Returns the index and the newEvents generated from the incoming blocking state event. Those new events will all created for the same effectiveDate and should be ordered but
-    // reOrderSubscriptionEventsOnSameDateByType would reorder them anyway if this was not the case.
-    //
-    private int insertFromBlockingEvent(final DateTimeZone accountTimeZone, final Set<UUID> allEntitlementUUIDs, final List<SubscriptionEvent> result, final BlockingState bs, final DateTime bsEffectiveDate, final List<SubscriptionEvent> newEvents) {
-        // Keep the current state per entitlement
-        final Map<UUID, TargetState> targetStates = new HashMap<UUID, TargetState>();
-        for (final UUID cur : allEntitlementUUIDs) {
-            targetStates.put(cur, new TargetState());
-        }
-
-        //
-        // Find out where to insert next event, and calculate current state for each entitlement at the position where we stop.
-        //
-        int index = -1;
-        final Iterator<SubscriptionEvent> it = result.iterator();
-        // Where we need to insert in that stream
-        DefaultSubscriptionEvent curInsertion = null;
-        while (it.hasNext()) {
-            final DefaultSubscriptionEvent cur = (DefaultSubscriptionEvent) it.next();
-            final int compEffectiveDate = bsEffectiveDate.compareTo(cur.getEffectiveDateTime());
-            final boolean shouldContinue = (compEffectiveDate >= 0);
-            if (!shouldContinue) {
-                break;
-            }
-            index++;
-
-            final TargetState curTargetState = targetStates.get(cur.getEntitlementId());
-            switch (cur.getSubscriptionEventType()) {
-                case START_ENTITLEMENT:
-                    curTargetState.setEntitlementStarted();
-                    break;
-                case STOP_ENTITLEMENT:
-                    curTargetState.setEntitlementStopped();
-                    break;
-                case START_BILLING:
-                    curTargetState.setBillingStarted();
-                    break;
-                case PAUSE_BILLING:
-                case PAUSE_ENTITLEMENT:
-                case RESUME_ENTITLEMENT:
-                case RESUME_BILLING:
-                case SERVICE_STATE_CHANGE:
-                    curTargetState.addEntitlementEvent(cur);
-                    break;
-                case STOP_BILLING:
-                    curTargetState.setBillingStopped();
-                    break;
-            }
-            curInsertion = cur;
-        }
-
-        // Extract the list of targets based on the type of blocking state
-        final List<UUID> targetEntitlementIds = bs.getType() == BlockingStateType.SUBSCRIPTION ? ImmutableList.<UUID>of(bs.getBlockedId()) :
-                                                ImmutableList.<UUID>copyOf(allEntitlementUUIDs);
-
-        // For each target compute the new events that should be inserted in the stream
-        for (final UUID targetEntitlementId : targetEntitlementIds) {
-            final SubscriptionEvent[] prevNext = findPrevNext(result, targetEntitlementId, curInsertion);
-            final TargetState curTargetState = targetStates.get(targetEntitlementId);
-
-            final List<SubscriptionEventType> eventTypes = curTargetState.addStateAndReturnEventTypes(bs);
-            for (final SubscriptionEventType t : eventTypes) {
-                newEvents.add(toSubscriptionEvent(prevNext[0], prevNext[1], targetEntitlementId, bs, t, accountTimeZone));
-            }
-        }
-        return index;
-    }
-
-    // Extract prev and next events in the stream events for that particular target subscription from the insertionEvent
-    private SubscriptionEvent[] findPrevNext(final List<SubscriptionEvent> events, final UUID targetEntitlementId, final SubscriptionEvent insertionEvent) {
-        // Find prev/next event for the same entitlement
-        final SubscriptionEvent[] result = new DefaultSubscriptionEvent[2];
-        if (insertionEvent == null) {
-            result[0] = null;
-            result[1] = !events.isEmpty() ? events.get(0) : null;
-            return result;
-        }
-
-        final Iterator<SubscriptionEvent> it = events.iterator();
-        DefaultSubscriptionEvent prev = null;
-        DefaultSubscriptionEvent next = null;
-        boolean foundCur = false;
-        while (it.hasNext()) {
-            final DefaultSubscriptionEvent tmp = (DefaultSubscriptionEvent) it.next();
-            if (tmp.getEntitlementId().equals(targetEntitlementId)) {
-                if (!foundCur) {
-                    prev = tmp;
-                } else {
-                    next = tmp;
-                    break;
-                }
-            }
-            // Check both the id and the event type because of multiplexing
-            if (tmp.getId().equals(insertionEvent.getId()) &&
-                tmp.getSubscriptionEventType().equals(insertionEvent.getSubscriptionEventType())) {
-                foundCur = true;
-            }
-        }
-        result[0] = prev;
-        result[1] = next;
-        return result;
-    }
-
-    // Compute the initial stream of events based on the subscription base events
-    private LinkedList<SubscriptionEvent> computeSubscriptionBaseEvents(final Collection<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
-        final LinkedList<SubscriptionEvent> result = new LinkedList<SubscriptionEvent>();
-        for (final Entitlement cur : entitlements) {
-            final SubscriptionBase base = ((DefaultEntitlement) cur).getSubscriptionBase();
-            final List<SubscriptionBaseTransition> baseTransitions = base.getAllTransitions();
-            for (final SubscriptionBaseTransition tr : baseTransitions) {
-                final List<SubscriptionEventType> eventTypes = toEventTypes(tr.getTransitionType());
-                for (final SubscriptionEventType eventType : eventTypes) {
-                    final SubscriptionEvent event = toSubscriptionEvent(tr, eventType, accountTimeZone);
-                    insertSubscriptionEvent(event, result);
-                }
-            }
-        }
-
-        return result;
-    }
-
-    private void insertSubscriptionEvent(final SubscriptionEvent event, final List<SubscriptionEvent> result) {
-        int index = 0;
-        for (final SubscriptionEvent cur : result) {
-            final int compEffectiveDate = event.getEffectiveDate().compareTo(cur.getEffectiveDate());
-            if (compEffectiveDate < 0) {
-                // EffectiveDate is less than cur -> insert here
-                break;
-            } else if (compEffectiveDate == 0) {
-                final int compUUID = event.getEntitlementId().compareTo(cur.getEntitlementId());
-                if (compUUID < 0) {
-                    // Same EffectiveDate but subscription are different, no need top sort further just return something deterministic
-                    break;
-                } else if (compUUID == 0) {
-                    final int eventOrder = event.getSubscriptionEventType().ordinal() - cur.getSubscriptionEventType().ordinal();
-                    if (eventOrder < 0) {
-                        // Same EffectiveDate and same subscription, order by SubscriptionEventType;
-                        break;
-                    }
-
-                    // Two identical events for the same subscription in the same day, trust createdDate
-                    if (eventOrder == 0) {
-                        final int compCreatedDate = (((DefaultSubscriptionEvent) event).getCreatedDate()).compareTo(((DefaultSubscriptionEvent) cur).getCreatedDate());
-                        if (compCreatedDate <= 0) {
-                            break;
-                        }
-                    }
-                }
-            }
-            index++;
-        }
-        result.add(index, event);
-    }
-
-    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();
-        }
-
-        // See https://github.com/killbill/killbill/issues/135
-        final String serviceName = getRealServiceNameForEntitlementOrExternalServiceName(in.getService(), eventType);
-
-        return new DefaultSubscriptionEvent(in.getId(),
-                                            entitlementId,
-                                            in.getEffectiveDate(),
-                                            in.getCreatedDate(),
-                                            eventType,
-                                            in.isBlockEntitlement(),
-                                            in.isBlockBilling(),
-                                            serviceName,
-                                            in.getStateName(),
-                                            prevProduct,
-                                            prevPlan,
-                                            prevPlanPhase,
-                                            prevPriceList,
-                                            prevBillingPeriod,
-                                            nextProduct,
-                                            nextPlan,
-                                            nextPlanPhase,
-                                            nextPriceList,
-                                            nextBillingPeriod,
-                                            in.getCreatedDate(),
-                                            accountTimeZone);
-    }
-
-    private static String getRealServiceNameForEntitlementOrExternalServiceName(final String originalServiceName, final SubscriptionEventType eventType) {
-        final String serviceName;
-        if (DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(originalServiceName)) {
-            serviceName = getServiceName(eventType);
-        } else {
-            serviceName = originalServiceName;
-        }
-        return serviceName;
-    }
-
-    private SubscriptionEvent toSubscriptionEvent(final SubscriptionBaseTransition in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
-        return new DefaultSubscriptionEvent(in.getId(),
-                                            in.getSubscriptionId(),
-                                            in.getEffectiveTransitionTime(),
-                                            in.getRequestedTransitionTime(),
-                                            eventType,
-                                            false,
-                                            false,
-                                            getServiceName(eventType),
-                                            eventType.toString(),
-                                            (in.getPreviousPlan() != null ? in.getPreviousPlan().getProduct() : null),
-                                            in.getPreviousPlan(),
-                                            in.getPreviousPhase(),
-                                            in.getPreviousPriceList(),
-                                            (in.getPreviousPlan() != null ? in.getPreviousPlan().getRecurringBillingPeriod() : null),
-                                            (in.getNextPlan() != null ? in.getNextPlan().getProduct() : null),
-                                            in.getNextPlan(),
-                                            in.getNextPhase(),
-                                            in.getNextPriceList(),
-                                            (in.getNextPlan() != null ? in.getNextPlan().getRecurringBillingPeriod() : null),
-                                            in.getCreatedDate(),
-                                            accountTimeZone);
-    }
-
-    private static String getServiceName(final SubscriptionEventType type) {
-        switch (type) {
-            case START_BILLING:
-            case PAUSE_BILLING:
-            case RESUME_BILLING:
-            case STOP_BILLING:
-                return BILLING_SERVICE_NAME;
-
-            case PHASE:
-            case CHANGE:
-                return ENT_BILLING_SERVICE_NAME;
-
-            default:
-                return DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME;
-        }
-    }
-
-    private List<SubscriptionEventType> toEventTypes(final SubscriptionBaseTransitionType in) {
-        switch (in) {
-            case CREATE:
-                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT, SubscriptionEventType.START_BILLING);
-            case TRANSFER:
-                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT, SubscriptionEventType.START_BILLING);
-            case MIGRATE_ENTITLEMENT:
-                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT);
-            case MIGRATE_BILLING:
-                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_BILLING);
-            case CHANGE:
-                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.CHANGE);
-            case CANCEL:
-                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.STOP_BILLING);
-            case PHASE:
-                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.PHASE);
-            /*
-             * Those can be ignored:
-             */
-            // Marker event
-            case UNCANCEL:
-                // Junction billing events-- that info is part of blocking states, we will get outside of subscription base
-            case START_BILLING_DISABLED:
-            case END_BILLING_DISABLED:
-            default:
-                return ImmutableList.<SubscriptionEventType>of();
-        }
+        this.events = SubscriptionEventOrdering.sortedCopy(entitlements, accountTimeZone);
     }
 
     @Override
@@ -622,129 +57,50 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         return events;
     }
 
-    //
-    // Internal class to keep the state associated with each subscription
-    //
-    private static final class TargetState {
-
-        private boolean isEntitlementStarted;
-        private boolean isEntitlementStopped;
-        private boolean isBillingStarted;
-        private boolean isBillingStopped;
-        private Map<String, BlockingState> perServiceBlockingState;
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultSubscriptionBundleTimeline{");
+        sb.append("accountId=").append(accountId);
+        sb.append(", bundleId=").append(bundleId);
+        sb.append(", externalKey='").append(externalKey).append('\'');
+        sb.append(", events=").append(events);
+        sb.append('}');
+        return sb.toString();
+    }
 
-        public TargetState() {
-            this.isEntitlementStarted = false;
-            this.isEntitlementStopped = false;
-            this.isBillingStarted = false;
-            this.isBillingStopped = false;
-            this.perServiceBlockingState = new HashMap<String, BlockingState>();
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
         }
-
-        public void setEntitlementStarted() {
-            isEntitlementStarted = true;
+        if (o == null || getClass() != o.getClass()) {
+            return false;
         }
 
-        public void setEntitlementStopped() {
-            isEntitlementStopped = true;
-        }
+        final DefaultSubscriptionBundleTimeline that = (DefaultSubscriptionBundleTimeline) o;
 
-        public void setBillingStarted() {
-            isBillingStarted = true;
+        if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
+            return false;
         }
-
-        public void setBillingStopped() {
-            isBillingStopped = true;
+        if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+            return false;
         }
-
-        public void addEntitlementEvent(final SubscriptionEvent e) {
-            final String serviceName = getRealServiceNameForEntitlementOrExternalServiceName(e.getServiceName(), e.getSubscriptionEventType());
-            final BlockingState lastBlockingStateForService = perServiceBlockingState.get(serviceName);
-
-            // Assume the event has no impact on changes - TODO this is wrong for SERVICE_STATE_CHANGE
-            final boolean blockChange = lastBlockingStateForService != null && lastBlockingStateForService.isBlockChange();
-            // For block entitlement or billing, override the previous state
-            final boolean blockedEntitlement = e.isBlockedEntitlement();
-            final boolean blockedBilling = e.isBlockedBilling();
-
-            final BlockingState converted = new DefaultBlockingState(e.getEntitlementId(),
-                                                                     BlockingStateType.SUBSCRIPTION,
-                                                                     e.getServiceStateName(),
-                                                                     serviceName,
-                                                                     blockChange,
-                                                                     blockedEntitlement,
-                                                                     blockedBilling,
-                                                                     ((DefaultSubscriptionEvent) e).getEffectiveDateTime());
-            perServiceBlockingState.put(converted.getService(), converted);
+        if (events != null ? !events.equals(that.events) : that.events != null) {
+            return false;
         }
-
-        //
-        // 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(),
-                                                                              bs.getStateName(),
-                                                                              bs.getService(),
-                                                                              bs.isBlockChange(),
-                                                                              (bs.isBlockEntitlement() && isEntitlementStarted && !isEntitlementStopped),
-                                                                              (bs.isBlockBilling() && isBillingStarted && !isBillingStopped),
-                                                                              bs.getEffectiveDate());
-
-            final List<SubscriptionEventType> result = new ArrayList<SubscriptionEventType>(4);
-            if (fixedBlockingState.getStateName().equals(DefaultEntitlementApi.ENT_STATE_CANCELLED)) {
-                isEntitlementStopped = true;
-                result.add(SubscriptionEventType.STOP_ENTITLEMENT);
-                return result;
-            }
-
-            //
-            // We look at the effect of the incoming event for the specific service, and then recompute the state after so we can compare if anything has changed
-            // across all services
-            //
-            final BlockingAggregator stateBefore = getState();
-            if (DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(fixedBlockingState.getService())) {
-                // Some blocking states will be added as entitlement-service and billing-service via addEntitlementEvent
-                // (see above). Because of it, we need to multiplex entitlement events here.
-                // TODO - this is magic and fragile. We should revisit how we create this state machine.
-                perServiceBlockingState.put(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME, fixedBlockingState);
-                perServiceBlockingState.put(DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME, fixedBlockingState);
-            } else {
-                perServiceBlockingState.put(fixedBlockingState.getService(), fixedBlockingState);
-            }
-            final BlockingAggregator stateAfter = getState();
-
-            final boolean shouldResumeEntitlement = isEntitlementStarted && !isEntitlementStopped && stateBefore.isBlockEntitlement() && !stateAfter.isBlockEntitlement();
-            if (shouldResumeEntitlement) {
-                result.add(SubscriptionEventType.RESUME_ENTITLEMENT);
-            }
-            final boolean shouldResumeBilling = isBillingStarted && !isBillingStopped && stateBefore.isBlockBilling() && !stateAfter.isBlockBilling();
-            if (shouldResumeBilling) {
-                result.add(SubscriptionEventType.RESUME_BILLING);
-            }
-
-            final boolean shouldBlockEntitlement = isEntitlementStarted && !isEntitlementStopped && !stateBefore.isBlockEntitlement() && stateAfter.isBlockEntitlement();
-            if (shouldBlockEntitlement) {
-                result.add(SubscriptionEventType.PAUSE_ENTITLEMENT);
-            }
-            final boolean shouldBlockBilling = isBillingStarted && !isBillingStopped && !stateBefore.isBlockBilling() && stateAfter.isBlockBilling();
-            if (shouldBlockBilling) {
-                result.add(SubscriptionEventType.PAUSE_BILLING);
-            }
-
-            if (!shouldResumeEntitlement && !shouldResumeBilling && !shouldBlockEntitlement && !shouldBlockBilling && !fixedBlockingState.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
-                result.add(SubscriptionEventType.SERVICE_STATE_CHANGE);
-            }
-            return result;
+        if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
+            return false;
         }
 
-        private BlockingAggregator getState() {
-            final DefaultBlockingAggregator aggrBefore = new DefaultBlockingAggregator();
-            for (final BlockingState cur : perServiceBlockingState.values()) {
-                aggrBefore.or(cur);
-            }
-            return aggrBefore;
-        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = accountId != null ? accountId.hashCode() : 0;
+        result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+        result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+        result = 31 * result + (events != null ? events.hashCode() : 0);
+        return result;
     }
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementOrderingBase.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementOrderingBase.java
new file mode 100644
index 0000000..6643f92
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementOrderingBase.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.entitlement.api;
+
+import org.killbill.billing.entitlement.DefaultEntitlementService;
+
+public abstract class EntitlementOrderingBase {
+
+    public static final String BILLING_SERVICE_NAME = "billing-service";
+    public static final String ENT_BILLING_SERVICE_NAME = "entitlement+billing-service";
+
+    protected static String getRealServiceNameForEntitlementOrExternalServiceName(final String originalServiceName, final SubscriptionEventType eventType) {
+        final String serviceName;
+        if (DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(originalServiceName)) {
+            serviceName = getServiceName(eventType);
+        } else {
+            serviceName = originalServiceName;
+        }
+        return serviceName;
+    }
+
+    protected static String getServiceName(final SubscriptionEventType type) {
+        switch (type) {
+            case START_BILLING:
+            case PAUSE_BILLING:
+            case RESUME_BILLING:
+            case STOP_BILLING:
+                return BILLING_SERVICE_NAME;
+
+            case PHASE:
+            case CHANGE:
+                return ENT_BILLING_SERVICE_NAME;
+
+            default:
+                return DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME;
+        }
+    }
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/SubscriptionEventOrdering.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/SubscriptionEventOrdering.java
new file mode 100644
index 0000000..5d793a7
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/SubscriptionEventOrdering.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.entitlement.api;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTimeZone;
+import org.killbill.billing.entitlement.DefaultEntitlementService;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+//
+// Compute all events based on blocking states events and base subscription events
+// Note that:
+// - base subscription events are already ordered for each Entitlement and so when we reorder at the bundle level we try not to break that initial ordering
+// - blocking state events occur at various level (account, bundle and subscription) so for higher level, we need to dispatch that on each subscription.
+//
+public class SubscriptionEventOrdering extends EntitlementOrderingBase {
+
+    @VisibleForTesting
+    static final SubscriptionEventOrdering INSTANCE = new SubscriptionEventOrdering();
+
+    private SubscriptionEventOrdering() {}
+
+    public static List<SubscriptionEvent> sortedCopy(final Entitlement entitlement, final DateTimeZone accountTimeZone) {
+        return sortedCopy(ImmutableList.<Entitlement>of(entitlement), accountTimeZone);
+    }
+
+    public static List<SubscriptionEvent> sortedCopy(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
+        return INSTANCE.computeEvents(entitlements, accountTimeZone);
+    }
+
+    private List<SubscriptionEvent> computeEvents(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
+        // Compute base events across all entitlements (already ordered per entitlement)
+        final LinkedList<SubscriptionEvent> result = computeSubscriptionBaseEvents(entitlements, accountTimeZone);
+
+        // Add blocking states at the right place
+        BlockingStateOrdering.insertSorted(entitlements, accountTimeZone, result);
+
+        // Final cleanups
+        reOrderSubscriptionEventsOnSameDateByType(result);
+        removeOverlappingSubscriptionEvents(result);
+
+        return result;
+    }
+
+    // Compute the initial stream of events based on the subscription base events
+    private LinkedList<SubscriptionEvent> computeSubscriptionBaseEvents(final Iterable<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
+        final LinkedList<SubscriptionEvent> result = new LinkedList<SubscriptionEvent>();
+        for (final Entitlement cur : entitlements) {
+            Preconditions.checkState(cur instanceof DefaultEntitlement, "Entitlement %s is not a DefaultEntitlement", cur);
+            final SubscriptionBase base = ((DefaultEntitlement) cur).getSubscriptionBase();
+            final List<SubscriptionBaseTransition> baseTransitions = base.getAllTransitions();
+            for (final SubscriptionBaseTransition tr : baseTransitions) {
+                final List<SubscriptionEventType> eventTypes = toEventTypes(tr.getTransitionType());
+                for (final SubscriptionEventType eventType : eventTypes) {
+                    final SubscriptionEvent event = toSubscriptionEvent(tr, eventType, accountTimeZone);
+                    insertSubscriptionEvent(event, result);
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private List<SubscriptionEventType> toEventTypes(final SubscriptionBaseTransitionType in) {
+        switch (in) {
+            case CREATE:
+                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT, SubscriptionEventType.START_BILLING);
+            case TRANSFER:
+                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT, SubscriptionEventType.START_BILLING);
+            case MIGRATE_ENTITLEMENT:
+                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_ENTITLEMENT);
+            case MIGRATE_BILLING:
+                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.START_BILLING);
+            case CHANGE:
+                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.CHANGE);
+            case CANCEL:
+                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.STOP_BILLING);
+            case PHASE:
+                return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.PHASE);
+            /*
+             * Those can be ignored:
+             */
+            // Marker event
+            case UNCANCEL:
+                // Junction billing events-- that info is part of blocking states, we will get outside of subscription base
+            case START_BILLING_DISABLED:
+            case END_BILLING_DISABLED:
+            default:
+                return ImmutableList.<SubscriptionEventType>of();
+        }
+    }
+
+    private void insertSubscriptionEvent(final SubscriptionEvent event, final List<SubscriptionEvent> result) {
+        int index = 0;
+        for (final SubscriptionEvent cur : result) {
+            final int compEffectiveDate = event.getEffectiveDate().compareTo(cur.getEffectiveDate());
+            if (compEffectiveDate < 0) {
+                // EffectiveDate is less than cur -> insert here
+                break;
+            } else if (compEffectiveDate == 0) {
+                final int compUUID = event.getEntitlementId().compareTo(cur.getEntitlementId());
+                if (compUUID < 0) {
+                    // Same EffectiveDate but subscription are different, no need top sort further just return something deterministic
+                    break;
+                } else if (compUUID == 0) {
+                    final int eventOrder = event.getSubscriptionEventType().ordinal() - cur.getSubscriptionEventType().ordinal();
+                    if (eventOrder < 0) {
+                        // Same EffectiveDate and same subscription, order by SubscriptionEventType;
+                        break;
+                    }
+
+                    // Two identical events for the same subscription in the same day, trust createdDate
+                    if (eventOrder == 0) {
+                        final int compCreatedDate = (((DefaultSubscriptionEvent) event).getCreatedDate()).compareTo(((DefaultSubscriptionEvent) cur).getCreatedDate());
+                        if (compCreatedDate <= 0) {
+                            break;
+                        }
+                    }
+                }
+            }
+            index++;
+        }
+        result.add(index, event);
+    }
+
+    private SubscriptionEvent toSubscriptionEvent(final SubscriptionBaseTransition in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
+        return new DefaultSubscriptionEvent(in.getId(),
+                                            in.getSubscriptionId(),
+                                            in.getEffectiveTransitionTime(),
+                                            in.getRequestedTransitionTime(),
+                                            eventType,
+                                            false,
+                                            false,
+                                            getServiceName(eventType),
+                                            eventType.toString(),
+                                            (in.getPreviousPlan() != null ? in.getPreviousPlan().getProduct() : null),
+                                            in.getPreviousPlan(),
+                                            in.getPreviousPhase(),
+                                            in.getPreviousPriceList(),
+                                            (in.getPreviousPlan() != null ? in.getPreviousPlan().getRecurringBillingPeriod() : null),
+                                            (in.getNextPlan() != null ? in.getNextPlan().getProduct() : null),
+                                            in.getNextPlan(),
+                                            in.getNextPhase(),
+                                            in.getNextPriceList(),
+                                            (in.getNextPlan() != null ? in.getNextPlan().getRecurringBillingPeriod() : null),
+                                            in.getCreatedDate(),
+                                            accountTimeZone);
+    }
+
+    //
+    // 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.
+    //
+    // All this seems a little over complicated, and one wonders why we don't just shove all events and call Collections.sort on the list prior
+    // to return:
+    // - One explanation is that we don't know the events in advance and each time the new events to be inserted are computed from the current state
+    //   of the stream, which requires ordering all along
+    // - A careful reader will notice that the algorithm is N^2, -- so that we care so much considering we have very events -- but in addition to that
+    //   the recursive path will be used very infrequently and when it is used, this will be probably just reorder with the prev event and that's it.
+    //
+    @VisibleForTesting
+    protected void reOrderSubscriptionEventsOnSameDateByType(final List<SubscriptionEvent> events) {
+        final int size = events.size();
+        for (int i = 0; i < size; i++) {
+            final SubscriptionEvent cur = events.get(i);
+            final SubscriptionEvent next = (i < (size - 1)) ? events.get(i + 1) : null;
+
+            final boolean shouldSwap = (next != null && shouldSwap(cur, next, true));
+            final boolean shouldReverseSort = (next == null || shouldSwap);
+
+            int currentIndex = i;
+            if (shouldSwap) {
+                Collections.swap(events, i, i + 1);
+            }
+            if (shouldReverseSort) {
+                while (currentIndex >= 1) {
+                    final SubscriptionEvent revCur = events.get(currentIndex);
+                    final SubscriptionEvent other = events.get(currentIndex - 1);
+                    if (shouldSwap(revCur, other, false)) {
+                        Collections.swap(events, currentIndex, currentIndex - 1);
+                    }
+                    if (revCur.getEffectiveDate().compareTo(other.getEffectiveDate()) != 0) {
+                        break;
+                    }
+                    currentIndex--;
+                }
+            }
+        }
+    }
+
+    // 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);
+            }
+        }
+    }
+
+    private boolean shouldSwap(final SubscriptionEvent cur, final SubscriptionEvent other, final boolean isAscending) {
+        // For a given date, order by subscriptionId, and within subscription by event type
+        final int idComp = cur.getEntitlementId().compareTo(other.getEntitlementId());
+        final Integer comparison = compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(cur, other);
+        return (cur.getEffectiveDate().compareTo(other.getEffectiveDate()) == 0 &&
+                ((isAscending &&
+                  ((idComp > 0) ||
+                   (idComp == 0 && comparison != null && comparison > 0))) ||
+                 (!isAscending &&
+                  ((idComp < 0) ||
+                   (idComp == 0 && comparison != null && comparison < 0)))));
+    }
+
+    private Integer compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(final SubscriptionEvent first, final SubscriptionEvent second) {
+        // For consistency, make sure entitlement-service and billing-service events always happen in a
+        // deterministic order (e.g. after other services for STOP events and before for START events)
+        if ((DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(first.getServiceName()) ||
+             BILLING_SERVICE_NAME.equals(first.getServiceName())) &&
+            !(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(second.getServiceName()) ||
+              BILLING_SERVICE_NAME.equals(second.getServiceName()))) {
+            // first is an entitlement-service or billing-service event, but not second
+            if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT) ||
+                first.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING) ||
+                first.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_ENTITLEMENT) ||
+                first.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_BILLING) ||
+                first.getSubscriptionEventType().equals(SubscriptionEventType.PHASE) ||
+                first.getSubscriptionEventType().equals(SubscriptionEventType.CHANGE)) {
+                return -1;
+            } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_ENTITLEMENT) ||
+                       first.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_BILLING) ||
+                       first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT) ||
+                       first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
+                return 1;
+            } else {
+                // Default behavior
+                return -1;
+            }
+        } else if ((DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(second.getServiceName()) ||
+                    BILLING_SERVICE_NAME.equals(second.getServiceName())) &&
+                   !(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME.equals(first.getServiceName()) ||
+                     BILLING_SERVICE_NAME.equals(first.getServiceName()))) {
+            // second is an entitlement-service or billing-service event, but not first
+            if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT) ||
+                second.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING) ||
+                second.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_ENTITLEMENT) ||
+                second.getSubscriptionEventType().equals(SubscriptionEventType.RESUME_BILLING) ||
+                second.getSubscriptionEventType().equals(SubscriptionEventType.PHASE) ||
+                second.getSubscriptionEventType().equals(SubscriptionEventType.CHANGE)) {
+                return 1;
+            } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_ENTITLEMENT) ||
+                       second.getSubscriptionEventType().equals(SubscriptionEventType.PAUSE_BILLING) ||
+                       second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT) ||
+                       second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
+                return -1;
+            } else {
+                // Default behavior
+                return 1;
+            }
+        } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT)) {
+            // START_ENTITLEMENT is always first
+            return -1;
+        } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_ENTITLEMENT)) {
+            // START_ENTITLEMENT is always first
+            return 1;
+        } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
+            // STOP_BILLING is always last
+            return 1;
+        } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_BILLING)) {
+            // STOP_BILLING is always last
+            return -1;
+        } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING)) {
+            // START_BILLING is first after START_ENTITLEMENT
+            return -1;
+        } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.START_BILLING)) {
+            // START_BILLING is first after START_ENTITLEMENT
+            return 1;
+        } else if (first.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT)) {
+            // STOP_ENTITLEMENT is last after STOP_BILLING
+            return 1;
+        } else if (second.getSubscriptionEventType().equals(SubscriptionEventType.STOP_ENTITLEMENT)) {
+            // STOP_ENTITLEMENT is last after STOP_BILLING
+            return -1;
+        } else {
+            // Trust the current ordering
+            return null;
+        }
+    }
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
index 337ad3b..b9f5c6a 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
@@ -17,18 +17,13 @@
 package org.killbill.billing.entitlement.api;
 
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
-import org.mockito.Mockito;
-import org.testng.Assert;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.PlanPhase;
@@ -36,12 +31,17 @@ import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.entitlement.DefaultEntitlementService;
 import org.killbill.billing.entitlement.EntitlementTestSuiteNoDB;
+import org.killbill.billing.entitlement.EventsStream;
 import org.killbill.billing.junction.DefaultBlockingState;
 import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType;
 import org.killbill.billing.subscription.events.user.ApiEventType;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
 
@@ -60,8 +60,8 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
     public class TestSubscriptionBundleTimeline extends DefaultSubscriptionBundleTimeline {
 
-        public TestSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final List<Entitlement> entitlements, final List<BlockingState> allBlockingStates) {
-            super(accountTimeZone, accountId, bundleId, externalKey, entitlements, allBlockingStates);
+        public TestSubscriptionBundleTimeline(final DateTimeZone accountTimeZone, final UUID accountId, final UUID bundleId, final String externalKey, final Iterable<Entitlement> entitlements) {
+            super(accountTimeZone, accountId, bundleId, externalKey, entitlements);
         }
 
         public SubscriptionEvent createEvent(final UUID subscriptionId, final SubscriptionEventType type, final DateTime effectiveDate) {
@@ -92,7 +92,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
     @Test(groups = "fast")
     public void testReOrderSubscriptionEventsOnInvalidOrder1() {
-        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
 
         final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
         final UUID subscriptionId = UUID.randomUUID();
@@ -102,7 +102,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
 
-        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+        SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
 
         Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -112,7 +112,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
     @Test(groups = "fast")
     public void testReOrderSubscriptionEventsOnInvalidOrder2() {
-        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
 
         final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
         final UUID subscriptionId = UUID.randomUUID();
@@ -122,7 +122,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
 
-        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+        SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
 
         Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -132,7 +132,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
     @Test(groups = "fast")
     public void testReOrderSubscriptionEventsOnInvalidOrder3() {
-        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
 
         final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
         final UUID subscriptionId = UUID.randomUUID();
@@ -142,7 +142,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_ENTITLEMENT, effectiveDate));
 
-        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+        SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
 
         Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -152,7 +152,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
     @Test(groups = "fast")
     public void testReOrderSubscriptionEventsOnInvalidOrderAndDifferentSubscriptionsSameDates1() {
-        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
 
         final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
         final UUID subscriptionId = UUID.fromString("60b64e0c-cefd-48c3-8de9-c731a9558165");
@@ -166,7 +166,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
 
-        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+        SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
 
         Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
         Assert.assertEquals(events.get(0).getEntitlementId(), otherSubscriptionId);
@@ -179,7 +179,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
     @Test(groups = "fast")
     public void testReOrderSubscriptionEventsOnInvalidOrderAndDifferentSubscriptionsSameDates2() {
-        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
 
         final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
         final UUID subscriptionId = UUID.fromString("35b3b340-31b2-46ea-b062-e9fc9fab3bc9");
@@ -193,7 +193,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
 
-        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+        SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
 
         Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -206,7 +206,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
     @Test(groups = "fast")
     public void testReOrderSubscriptionEventsOnInvalidOrderAndDifferentSubscriptionsDates() {
-        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
 
         final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
         final UUID subscriptionId = UUID.randomUUID();
@@ -227,7 +227,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.PAUSE_ENTITLEMENT, otherEffectiveDate));
         events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.PAUSE_BILLING, otherEffectiveDate));
 
-        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+        SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
 
         Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -244,7 +244,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
 
     @Test(groups = "fast")
     public void testReOrderSubscriptionEventsOnCorrectOrder() {
-        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+        final TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>());
 
         final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
         final UUID subscriptionId = UUID.randomUUID();
@@ -254,7 +254,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
         events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
 
-        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+        SubscriptionEventOrdering.INSTANCE.reOrderSubscriptionEventsOnSameDateByType(events);
 
         Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -294,7 +294,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
         entitlements.add(entitlement);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, Collections.<BlockingState>emptyList());
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         assertEquals(timeline.getAccountId(), accountId);
         assertEquals(timeline.getBundleId(), bundleId);
@@ -314,9 +314,9 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
-        assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
-        assertEquals(events.get(3).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
+        assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
+        assertEquals(events.get(3).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -362,10 +362,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         allTransitions.add(tr2);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
         entitlements.add(entitlement);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         assertEquals(timeline.getAccountId(), accountId);
         assertEquals(timeline.getBundleId(), bundleId);
@@ -385,9 +385,9 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
         assertEquals(events.get(2).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(3).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(3).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -457,10 +457,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         blockingStates.add(bs4);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
         entitlements.add(entitlement);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         assertEquals(timeline.getAccountId(), accountId);
         assertEquals(timeline.getBundleId(), bundleId);
@@ -493,14 +493,14 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.RESUME_BILLING);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
-        assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+        assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
 
         assertEquals(events.get(3).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(4).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(4).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
         assertEquals(events.get(5).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(6).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(6).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertEquals(events.get(7).getServiceName(), service);
         assertEquals(events.get(8).getServiceName(), service);
@@ -596,10 +596,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         allTransitions.add(tr3);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
         entitlements.add(entitlement);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         assertEquals(timeline.getAccountId(), accountId);
         assertEquals(timeline.getBundleId(), bundleId);
@@ -636,9 +636,9 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(9).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
-        assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+        assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
 
         assertEquals(events.get(3).getServiceName(), overdueService);
         assertEquals(events.get(4).getServiceName(), overdueService);
@@ -648,7 +648,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(7).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
 
         assertEquals(events.get(8).getServiceName(), overdueService);
-        assertEquals(events.get(9).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(9).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -720,10 +720,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         allTransitions.add(tr3);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
         entitlements.add(entitlement);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         assertEquals(timeline.getAccountId(), accountId);
         assertEquals(timeline.getBundleId(), bundleId);
@@ -745,10 +745,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
-        assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
+        assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
         assertEquals(events.get(3).getServiceName(), service);
-        assertEquals(events.get(4).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(4).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -804,10 +804,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         blockingStates.add(bs2);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
         entitlements.add(entitlement);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
         assertEquals(events.size(), 6);
@@ -827,11 +827,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
-        assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
+        assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
         assertEquals(events.get(3).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
         assertEquals(events.get(4).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(5).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(5).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -899,13 +899,13 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         blockingStates.add(bs2);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement1 = createEntitlement(entitlementId1, allTransitions1);
+        final Entitlement entitlement1 = createEntitlement(entitlementId1, allTransitions1, blockingStates);
         entitlements.add(entitlement1);
 
-        final Entitlement entitlement2 = createEntitlement(entitlementId2, allTransitions2);
+        final Entitlement entitlement2 = createEntitlement(entitlementId2, allTransitions2, blockingStates);
         entitlements.add(entitlement2);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
         assertEquals(events.size(), 9);
@@ -937,17 +937,17 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
         assertEquals(events.get(2).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(3).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(3).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
-        assertEquals(events.get(4).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+        assertEquals(events.get(4).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
 
         assertEquals(events.get(5).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
         assertEquals(events.get(6).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
 
         assertEquals(events.get(7).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(8).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(8).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial1");
@@ -1006,8 +1006,8 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         allTransitions.add(tr3);
 
         // Verify the timeline without the blocking state events
-        final ImmutableList<Entitlement> entitlementsWithoutBlockingStates = ImmutableList.<Entitlement>of(createEntitlement(entitlementId, allTransitions));
-        final List<SubscriptionEvent> eventsWithoutBlockingStates = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlementsWithoutBlockingStates, blockingStates).getSubscriptionEvents();
+        final ImmutableList<Entitlement> entitlementsWithoutBlockingStates = ImmutableList.<Entitlement>of(createEntitlement(entitlementId, allTransitions, blockingStates));
+        final List<SubscriptionEvent> eventsWithoutBlockingStates = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlementsWithoutBlockingStates).getSubscriptionEvents();
         assertEquals(eventsWithoutBlockingStates.size(), 4);
         assertEquals(eventsWithoutBlockingStates.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         assertEquals(eventsWithoutBlockingStates.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -1022,8 +1022,8 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         blockingStates.add(bs1);
 
         // Verify the timeline with the overdue event blocking the entitlement
-        final ImmutableList<Entitlement> entitlementsWithOverdueEvent = ImmutableList.<Entitlement>of(createEntitlement(entitlementId, allTransitions));
-        final List<SubscriptionEvent> eventsWithOverdueEvent = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlementsWithOverdueEvent, blockingStates).getSubscriptionEvents();
+        final ImmutableList<Entitlement> entitlementsWithOverdueEvent = ImmutableList.<Entitlement>of(createEntitlement(entitlementId, allTransitions, blockingStates));
+        final List<SubscriptionEvent> eventsWithOverdueEvent = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlementsWithOverdueEvent).getSubscriptionEvents();
         assertEquals(eventsWithOverdueEvent.size(), 5);
         assertEquals(eventsWithOverdueEvent.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         assertEquals(eventsWithOverdueEvent.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
@@ -1038,11 +1038,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         blockingStates.add(bs2);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions,  blockingStates);
         entitlements.add(entitlement);
 
         // Verify the timeline with both the overdue event and the entitlement cancel event
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         assertEquals(timeline.getAccountId(), accountId);
         assertEquals(timeline.getBundleId(), bundleId);
@@ -1066,11 +1066,11 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
-        assertEquals(events.get(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
+        assertEquals(events.get(2).getServiceName(), EntitlementOrderingBase.ENT_BILLING_SERVICE_NAME);
         assertEquals(events.get(3).getServiceName(), service);
         assertEquals(events.get(4).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(5).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(5).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");
@@ -1139,10 +1139,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         blockingStates.add(bs4);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
         entitlements.add(entitlement);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
         assertEquals(events.size(), 4);
@@ -1158,7 +1158,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
         assertEquals(events.get(2).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
         assertEquals(events.get(3).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
 
@@ -1232,10 +1232,10 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         blockingStates.add(bs5);
 
         final List<Entitlement> entitlements = new ArrayList<Entitlement>();
-        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions, blockingStates);
         entitlements.add(entitlement);
 
-        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements);
 
         final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
         assertEquals(events.size(), 11);
@@ -1270,17 +1270,17 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(10).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
 
         assertEquals(events.get(0).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(1).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(1).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertEquals(events.get(2).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(3).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(3).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
         assertEquals(events.get(4).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(5).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(5).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertEquals(events.get(6).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(7).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(7).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
         assertEquals(events.get(8).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(9).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+        assertEquals(events.get(9).getServiceName(), EntitlementOrderingBase.BILLING_SERVICE_NAME);
 
         assertEquals(events.get(10).getServiceName(), overdueService);
 
@@ -1312,9 +1312,17 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
     }
 
     private Entitlement createEntitlement(final UUID entitlementId, final List<SubscriptionBaseTransition> allTransitions) {
+        return createEntitlement(entitlementId, allTransitions, ImmutableList.<BlockingState>of());
+    }
+
+    private Entitlement createEntitlement(final UUID entitlementId, final List<SubscriptionBaseTransition> allTransitions, final Collection<BlockingState> blockingStates) {
         final DefaultEntitlement result = Mockito.mock(DefaultEntitlement.class);
         Mockito.when(result.getId()).thenReturn(entitlementId);
 
+        final EventsStream eventsStream = Mockito.mock(EventsStream.class);
+        Mockito.when(eventsStream.getBlockingStates()).thenReturn(blockingStates);
+        Mockito.when(result.getEventsStream()).thenReturn(eventsStream);
+
         final SubscriptionBase base = Mockito.mock(SubscriptionBase.class);
         Mockito.when(base.getAllTransitions()).thenReturn(allTransitions);
         Mockito.when(result.getSubscriptionBase()).thenReturn(base);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
index 9c3a1f0..22effcd 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleJson.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -23,39 +25,49 @@ import javax.annotation.Nullable;
 
 import org.killbill.billing.entitlement.api.Subscription;
 import org.killbill.billing.entitlement.api.SubscriptionBundle;
-import org.killbill.billing.entitlement.api.SubscriptionEvent;
 import org.killbill.billing.util.audit.AccountAuditLogs;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
 import com.wordnik.swagger.annotations.ApiModelProperty;
 
 public class BundleJson extends JsonBase {
 
     @ApiModelProperty(dataType = "java.util.UUID", required = true)
-    protected final String accountId;
+    private final String accountId;
     @ApiModelProperty(dataType = "java.util.UUID")
-    protected final String bundleId;
-    protected final String externalKey;
+    private final String bundleId;
+    private final String externalKey;
     private final List<SubscriptionJson> subscriptions;
+    private final BundleTimelineJson timeline;
 
     @JsonCreator
     public BundleJson(@JsonProperty("accountId") @Nullable final String accountId,
                       @JsonProperty("bundleId") @Nullable final String bundleId,
                       @JsonProperty("externalKey") @Nullable final String externalKey,
                       @JsonProperty("subscriptions") @Nullable final List<SubscriptionJson> subscriptions,
+                      @JsonProperty("timeline") @Nullable final BundleTimelineJson timeline,
                       @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
         super(auditLogs);
         this.accountId = accountId;
         this.bundleId = bundleId;
         this.externalKey = externalKey;
         this.subscriptions = subscriptions;
+        this.timeline = timeline;
+    }
+
+    public BundleJson(final SubscriptionBundle bundle, @Nullable final AccountAuditLogs accountAuditLogs) {
+        super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundle.getId())));
+        this.accountId = bundle.getAccountId().toString();
+        this.bundleId = bundle.getId().toString();
+        this.externalKey = bundle.getExternalKey();
+        this.subscriptions = new LinkedList<SubscriptionJson>();
+        for (final Subscription subscription : bundle.getSubscriptions()) {
+            this.subscriptions.add(new SubscriptionJson(subscription, accountAuditLogs));
+        }
+        this.timeline = new BundleTimelineJson(bundle.getTimeline(), accountAuditLogs);
     }
 
-    @JsonProperty("subscriptions")
     public List<SubscriptionJson> getSubscriptions() {
         return subscriptions;
     }
@@ -72,32 +84,20 @@ public class BundleJson extends JsonBase {
         return externalKey;
     }
 
-    public BundleJson(final SubscriptionBundle bundle, @Nullable final AccountAuditLogs accountAuditLogs) {
-        super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundle.getId())));
-        this.accountId = bundle.getAccountId().toString();
-        this.bundleId = bundle.getId().toString();
-        this.externalKey = bundle.getExternalKey();
-
-        this.subscriptions = new LinkedList<SubscriptionJson>();
-        for (final Subscription cur : bundle.getSubscriptions()) {
-            final ImmutableList<SubscriptionEvent> events = ImmutableList.<SubscriptionEvent>copyOf(Collections2.filter(bundle.getTimeline().getSubscriptionEvents(), new Predicate<SubscriptionEvent>() {
-                @Override
-                public boolean apply(@Nullable final SubscriptionEvent input) {
-                    return input.getEntitlementId().equals(cur.getId());
-                }
-            }));
-            this.subscriptions.add(new SubscriptionJson(cur, events, accountAuditLogs));
-        }
+    public BundleTimelineJson getTimeline() {
+        return timeline;
     }
 
     @Override
     public String toString() {
-        return "BundleJson{" +
-               "accountId='" + accountId + '\'' +
-               ", bundleId='" + bundleId + '\'' +
-               ", externalKey='" + externalKey + '\'' +
-               ", subscriptions=" + subscriptions +
-               '}';
+        final StringBuilder sb = new StringBuilder("BundleJson{");
+        sb.append("accountId='").append(accountId).append('\'');
+        sb.append(", bundleId='").append(bundleId).append('\'');
+        sb.append(", externalKey='").append(externalKey).append('\'');
+        sb.append(", subscriptions=").append(subscriptions);
+        sb.append(", timeline=").append(timeline);
+        sb.append('}');
+        return sb.toString();
     }
 
     @Override
@@ -123,6 +123,9 @@ public class BundleJson extends JsonBase {
         if (subscriptions != null ? !subscriptions.equals(that.subscriptions) : that.subscriptions != null) {
             return false;
         }
+        if (timeline != null ? !timeline.equals(that.timeline) : that.timeline != null) {
+            return false;
+        }
 
         return true;
     }
@@ -133,6 +136,7 @@ public class BundleJson extends JsonBase {
         result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
         result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
         result = 31 * result + (subscriptions != null ? subscriptions.hashCode() : 0);
+        result = 31 * result + (timeline != null ? timeline.hashCode() : 0);
         return result;
     }
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java
index 942daee..9dac4cb 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/BundleTimelineJson.java
@@ -16,55 +16,79 @@
 
 package org.killbill.billing.jaxrs.json;
 
+import java.util.LinkedList;
 import java.util.List;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class BundleTimelineJson {
+import javax.annotation.Nullable;
 
-    private final String viewId;
+import org.killbill.billing.entitlement.api.SubscriptionBundleTimeline;
+import org.killbill.billing.entitlement.api.SubscriptionEvent;
+import org.killbill.billing.jaxrs.json.SubscriptionJson.EventSubscriptionJson;
+import org.killbill.billing.util.audit.AccountAuditLogs;
 
-    private final BundleJson bundle;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.wordnik.swagger.annotations.ApiModelProperty;
 
-    private final List<InvoicePaymentJson> payments;
+public class BundleTimelineJson extends JsonBase {
 
-    private final List<InvoiceJson> invoices;
+    @ApiModelProperty(dataType = "java.util.UUID")
+    private final String accountId;
+    @ApiModelProperty(dataType = "java.util.UUID")
+    private final String bundleId;
+    private final String externalKey;
+    private final List<EventSubscriptionJson> events;
 
+    @JsonCreator
+    public BundleTimelineJson(@JsonProperty("accountId") @Nullable final String accountId,
+                              @JsonProperty("bundleId") @Nullable final String bundleId,
+                              @JsonProperty("externalKey") @Nullable final String externalKey,
+                              @JsonProperty("events") @Nullable final List<EventSubscriptionJson> events,
+                              @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(auditLogs);
+        this.accountId = accountId;
+        this.bundleId = bundleId;
+        this.externalKey = externalKey;
+        this.events = events;
+    }
 
-    private final String reasonForChange;
+    public BundleTimelineJson(final SubscriptionBundleTimeline bundleTimeline, @Nullable final AccountAuditLogs accountAuditLogs) {
+        super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForBundle(bundleTimeline.getBundleId())));
+        this.accountId = bundleTimeline.getAccountId().toString();
+        this.bundleId = bundleTimeline.getBundleId().toString();
+        this.externalKey = bundleTimeline.getExternalKey();
 
-    @JsonCreator
-    public BundleTimelineJson(@JsonProperty("viewId") final String viewId,
-                              @JsonProperty("bundle") final BundleJson bundle,
-                              @JsonProperty("payments") final List<InvoicePaymentJson> payments,
-                              @JsonProperty("invoices") final List<InvoiceJson> invoices,
-                              @JsonProperty("reasonForChange") final String reason) {
-        this.viewId = viewId;
-        this.bundle = bundle;
-        this.payments = payments;
-        this.invoices = invoices;
-        this.reasonForChange = reason;
+        this.events = new LinkedList<EventSubscriptionJson>();
+        for (final SubscriptionEvent subscriptionEvent : bundleTimeline.getSubscriptionEvents()) {
+            this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
+        }
     }
 
-    public String getViewId() {
-        return viewId;
+    public String getAccountId() {
+        return accountId;
     }
 
-    public BundleJson getBundle() {
-        return bundle;
+    public String getBundleId() {
+        return bundleId;
     }
 
-    public List<InvoicePaymentJson> getPayments() {
-        return payments;
+    public String getExternalKey() {
+        return externalKey;
     }
 
-    public List<InvoiceJson> getInvoices() {
-        return invoices;
+    public List<EventSubscriptionJson> getEvents() {
+        return events;
     }
 
-    public String getReasonForChange() {
-        return reasonForChange;
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("BundleTimelineJson{");
+        sb.append("accountId='").append(accountId).append('\'');
+        sb.append(", bundleId='").append(bundleId).append('\'');
+        sb.append(", externalKey='").append(externalKey).append('\'');
+        sb.append(", events=").append(events);
+        sb.append('}');
+        return sb.toString();
     }
 
     @Override
@@ -78,19 +102,16 @@ public class BundleTimelineJson {
 
         final BundleTimelineJson that = (BundleTimelineJson) o;
 
-        if (bundle != null ? !bundle.equals(that.bundle) : that.bundle != null) {
-            return false;
-        }
-        if (invoices != null ? !invoices.equals(that.invoices) : that.invoices != null) {
+        if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) {
             return false;
         }
-        if (payments != null ? !payments.equals(that.payments) : that.payments != null) {
+        if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
             return false;
         }
-        if (reasonForChange != null ? !reasonForChange.equals(that.reasonForChange) : that.reasonForChange != null) {
+        if (events != null ? !events.equals(that.events) : that.events != null) {
             return false;
         }
-        if (viewId != null ? !viewId.equals(that.viewId) : that.viewId != null) {
+        if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
             return false;
         }
 
@@ -99,11 +120,10 @@ public class BundleTimelineJson {
 
     @Override
     public int hashCode() {
-        int result = viewId != null ? viewId.hashCode() : 0;
-        result = 31 * result + (bundle != null ? bundle.hashCode() : 0);
-        result = 31 * result + (payments != null ? payments.hashCode() : 0);
-        result = 31 * result + (invoices != null ? invoices.hashCode() : 0);
-        result = 31 * result + (reasonForChange != null ? reasonForChange.hashCode() : 0);
+        int result = accountId != null ? accountId.hashCode() : 0;
+        result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+        result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
+        result = 31 * result + (events != null ? events.hashCode() : 0);
         return result;
     }
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
index 44aab8e..17e1f02 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
@@ -105,6 +105,26 @@ public class SubscriptionJson extends JsonBase {
             this.phase = phase;
         }
 
+        public EventSubscriptionJson(final SubscriptionEvent subscriptionEvent, @Nullable final AccountAuditLogs accountAuditLogs) {
+            super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscriptionEvent(subscriptionEvent.getId())));
+            final BillingPeriod billingPeriod = subscriptionEvent.getNextBillingPeriod() != null ? subscriptionEvent.getNextBillingPeriod() : subscriptionEvent.getPrevBillingPeriod();
+            final Product product = subscriptionEvent.getNextProduct() != null ? subscriptionEvent.getNextProduct() : subscriptionEvent.getPrevProduct();
+            final PriceList priceList = subscriptionEvent.getNextPriceList() != null ? subscriptionEvent.getNextPriceList() : subscriptionEvent.getPrevPriceList();
+            final PlanPhase phase = subscriptionEvent.getNextPhase() != null ? subscriptionEvent.getNextPhase() : subscriptionEvent.getPrevPhase();
+            this.eventId = subscriptionEvent.getId().toString();
+            this.billingPeriod = billingPeriod != null ? billingPeriod.toString() : null;
+            this.requestedDate = subscriptionEvent.getRequestedDate();
+            this.effectiveDate = subscriptionEvent.getEffectiveDate();
+            this.product = product != null ? product.getName() : null;
+            this.priceList = priceList != null ? priceList.getName() : null;
+            this.eventType = subscriptionEvent.getSubscriptionEventType().toString();
+            this.isBlockedBilling = subscriptionEvent.isBlockedBilling();
+            this.isBlockedEntitlement = subscriptionEvent.isBlockedEntitlement();
+            this.serviceName = subscriptionEvent.getServiceName();
+            this.serviceStateName = subscriptionEvent.getServiceStateName();
+            this.phase = phase != null ? phase.getName() : null;
+        }
+
         public String getEventId() {
             return eventId;
         }
@@ -274,9 +294,7 @@ public class SubscriptionJson extends JsonBase {
         this.events = events;
     }
 
-    public SubscriptionJson(final Subscription subscription,
-                            final List<SubscriptionEvent> subscriptionEvents,
-                            @Nullable final AccountAuditLogs accountAuditLogs) {
+    public SubscriptionJson(final Subscription subscription, @Nullable final AccountAuditLogs accountAuditLogs) {
         super(toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscription(subscription.getId())));
         this.startDate = subscription.getEffectiveStartDate();
         this.productName = subscription.getLastActiveProduct().getName();
@@ -291,27 +309,9 @@ public class SubscriptionJson extends JsonBase {
         this.bundleId = subscription.getBundleId().toString();
         this.subscriptionId = subscription.getId().toString();
         this.externalKey = subscription.getExternalKey();
-        this.events = subscriptionEvents != null ? new LinkedList<EventSubscriptionJson>() : null;
-        if (events != null) {
-            for (final SubscriptionEvent cur : subscriptionEvents) {
-                final BillingPeriod billingPeriod = cur.getNextBillingPeriod() != null ? cur.getNextBillingPeriod() : cur.getPrevBillingPeriod();
-                final Product product = cur.getNextProduct() != null ? cur.getNextProduct() : cur.getPrevProduct();
-                final PriceList priceList = cur.getNextPriceList() != null ? cur.getNextPriceList() : cur.getPrevPriceList();
-                final PlanPhase phase = cur.getNextPhase() != null ? cur.getNextPhase() : cur.getPrevPhase();
-                this.events.add(new EventSubscriptionJson(cur.getId().toString(),
-                                                          billingPeriod != null ? billingPeriod.toString() : null,
-                                                          cur.getRequestedDate(),
-                                                          cur.getEffectiveDate(),
-                                                          product != null ? product.getName() : null,
-                                                          priceList != null ? priceList.getName() : null,
-                                                          cur.getSubscriptionEventType().toString(),
-                                                          cur.isBlockedBilling(),
-                                                          cur.isBlockedEntitlement(),
-                                                          cur.getServiceName(),
-                                                          cur.getServiceStateName(),
-                                                          phase != null ? phase.getName() : null,
-                                                          toAuditLogJson(accountAuditLogs == null ? null : accountAuditLogs.getAuditLogsForSubscriptionEvent(cur.getId()))));
-            }
+        this.events = new LinkedList<EventSubscriptionJson>();
+        for (final SubscriptionEvent subscriptionEvent : subscription.getSubscriptionEvents()) {
+            this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
         }
     }
 
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index e83a503..f272f2c 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -127,7 +129,7 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                    @javax.ws.rs.core.Context final HttpServletRequest request) throws SubscriptionApiException {
         final UUID uuid = UUID.fromString(subscriptionId);
         final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(uuid, context.createContext(request));
-        final SubscriptionJson json = new SubscriptionJson(subscription, null, null);
+        final SubscriptionJson json = new SubscriptionJson(subscription, null);
         return Response.status(Status.OK).entity(json).build();
     }
 
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
index 6cf55c8..933d6bd 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
@@ -70,7 +70,7 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuiteNoDB {
                                                                    ImmutableList.<EventSubscriptionJson>of(event),
                                                                    auditLogs);
 
-        final BundleJson bundleJson = new BundleJson(someUUID, bundleId.toString(), externalKey, ImmutableList.<SubscriptionJson>of(subscription), auditLogs);
+        final BundleJson bundleJson = new BundleJson(someUUID, bundleId.toString(), externalKey, ImmutableList.<SubscriptionJson>of(subscription), null, auditLogs);
         Assert.assertEquals(bundleJson.getBundleId(), bundleId.toString());
         Assert.assertEquals(bundleJson.getExternalKey(), externalKey);
         Assert.assertEquals(bundleJson.getSubscriptions().size(), 1);
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
index 28e6a46..9874b55 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestBundleTimelineJson.java
@@ -18,85 +18,42 @@
 
 package org.killbill.billing.jaxrs.json;
 
-import java.math.BigDecimal;
 import java.util.UUID;
 
-import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.killbill.billing.jaxrs.json.SubscriptionJson.EventSubscriptionJson;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import org.killbill.billing.catalog.api.Currency;
-import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
-
 import com.google.common.collect.ImmutableList;
 
 public class TestBundleTimelineJson extends JaxrsTestSuiteNoDB {
 
     @Test(groups = "fast")
     public void testJson() throws Exception {
-        final String viewId = UUID.randomUUID().toString();
-        final String reason = UUID.randomUUID().toString();
-
-        final BundleJson bundleJson = createBundleWithSubscriptions();
-        final InvoiceJson invoiceJson = createInvoice();
-        final InvoicePaymentJson paymentJson = createPayment(UUID.fromString(invoiceJson.getAccountId()),
-                                                                  UUID.fromString(invoiceJson.getInvoiceId()));
-
-        final BundleTimelineJson bundleTimelineJson = new BundleTimelineJson(viewId,
-                                                                             bundleJson,
-                                                                             ImmutableList.<InvoicePaymentJson>of(paymentJson),
-                                                                             ImmutableList.<InvoiceJson>of(invoiceJson),
-                                                                             reason);
+        final EventSubscriptionJson event = new EventSubscriptionJson(UUID.randomUUID().toString(),
+                                                                      BillingPeriod.NO_BILLING_PERIOD.toString(),
+                                                                      new LocalDate(),
+                                                                      new LocalDate(),
+                                                                      UUID.randomUUID().toString(),
+                                                                      UUID.randomUUID().toString(),
+                                                                      UUID.randomUUID().toString(),
+                                                                      true,
+                                                                      false,
+                                                                      UUID.randomUUID().toString(),
+                                                                      UUID.randomUUID().toString(),
+                                                                      UUID.randomUUID().toString(),
+                                                                      null);
+        final BundleTimelineJson bundleTimelineJson = new BundleTimelineJson(UUID.randomUUID().toString(),
+                                                                             UUID.randomUUID().toString(),
+                                                                             UUID.randomUUID().toString(),
+                                                                             ImmutableList.<EventSubscriptionJson>of(event),
+                                                                             null);
 
         final String asJson = mapper.writeValueAsString(bundleTimelineJson);
         final BundleTimelineJson fromJson = mapper.readValue(asJson, BundleTimelineJson.class);
         Assert.assertEquals(fromJson, bundleTimelineJson);
     }
-
-    private BundleJson createBundleWithSubscriptions() {
-        final String someUUID = UUID.randomUUID().toString();
-        final UUID accountId = UUID.randomUUID();
-        final UUID bundleId = UUID.randomUUID();
-        final UUID subscriptionId = UUID.randomUUID();
-        final String externalKey = UUID.randomUUID().toString();
-
-        final SubscriptionJson entitlementJsonWithEvents = new SubscriptionJson(accountId.toString(), bundleId.toString(), subscriptionId.toString(), externalKey,
-                                                                                new LocalDate(), someUUID, someUUID, someUUID, someUUID,
-                                                                                new LocalDate(), new LocalDate(), new LocalDate(), new LocalDate(),
-                                                                                null, null);
-        return new BundleJson(accountId.toString(), bundleId.toString(), externalKey, ImmutableList.<SubscriptionJson>of(entitlementJsonWithEvents), null);
-    }
-
-    private InvoiceJson createInvoice() {
-        final UUID accountId = UUID.randomUUID();
-        final UUID invoiceId = UUID.randomUUID();
-        final BigDecimal invoiceAmount = BigDecimal.TEN;
-        final BigDecimal creditAdj = BigDecimal.ONE;
-        final BigDecimal refundAdj = BigDecimal.ONE;
-        final LocalDate invoiceDate = clock.getUTCToday();
-        final LocalDate targetDate = clock.getUTCToday();
-        final String invoiceNumber = UUID.randomUUID().toString();
-        final BigDecimal balance = BigDecimal.ZERO;
-
-        return new InvoiceJson(invoiceAmount, Currency.USD.toString(), creditAdj, refundAdj, invoiceId.toString(), invoiceDate,
-                                     targetDate, invoiceNumber, balance, accountId.toString(), null, null, null, null);
-    }
-
-    private InvoicePaymentJson createPayment(final UUID accountId, final UUID invoiceId) {
-        final UUID paymentId = UUID.randomUUID();
-        final Integer paymentNumber = 17;
-        final String paymentExternalKey = UUID.randomUUID().toString();
-        final BigDecimal authAmount = BigDecimal.TEN;
-        final BigDecimal captureAmount = BigDecimal.ZERO;
-        final BigDecimal purchasedAMount = BigDecimal.ZERO;
-        final BigDecimal creditAmount = BigDecimal.ZERO;
-        final BigDecimal refundAmount = BigDecimal.ZERO;
-        final String currency = "USD";
-
-        return new InvoicePaymentJson(invoiceId.toString(), accountId.toString(), paymentId.toString(), paymentNumber.toString(),
-                                      paymentExternalKey, authAmount, captureAmount, purchasedAMount, refundAmount, creditAmount, currency,
-                                      UUID.randomUUID().toString(),
-                                      null, null);
-    }
 }