killbill-memoizeit

Different implementation for re-ordering the timeline events

11/11/2013 3:15:42 PM

Details

diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
index fd409c6..060a5e0 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionBundleTimeline.java
@@ -49,6 +49,7 @@ import com.ning.billing.subscription.api.SubscriptionBase;
 import com.ning.billing.subscription.api.SubscriptionBaseTransitionType;
 import com.ning.billing.subscription.api.user.SubscriptionBaseTransition;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
@@ -58,6 +59,10 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
 
     private final Logger logger = LoggerFactory.getLogger(DefaultSubscriptionBundleTimeline.class);
 
+    // STEPH This is added to give us confidence the timeline we generate behaves as expected. Could be removed at some point
+    private final static String TIMELINE_WARN_LOG = "Sanity Timeline: ";
+
+
     public static final String BILLING_SERVICE_NAME = "billing-service";
     public static final String ENT_BILLING_SERVICE_NAME = "entitlement+billing-service";
 
@@ -101,7 +106,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                 if (effectivedComp != 0) {
                     return effectivedComp;
                 }
-                // For the same effectiveDate we want to first return ENTITLEMENT events
+                // For the same effectiveDate we want to first return events from ENTITLEMENT service first
                 final int serviceNameComp = o1.getService().compareTo(o2.getService());
                 if (serviceNameComp != 0) {
                     if (o1.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
@@ -112,11 +117,12 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                         return serviceNameComp;
                     }
                 }
+                // Order by subscription just to get something deterministic
                 final int uuidComp = o1.getBlockedId().compareTo(o2.getBlockedId());
                 if (uuidComp != 0) {
                     return uuidComp;
                 }
-                // Same effectiveDate, createdDate and for the same object, we sort first by serviceName and then serviceState
+                // And then finally state
                 final int serviceStateComp = o1.getStateName().compareTo(o2.getStateName());
                 if (serviceStateComp != 0) {
                     return serviceStateComp;
@@ -125,7 +131,9 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                 if (createdDateComp != 0) {
                     return createdDateComp;
                 }
-                // Underministic-- not sure that will ever happen.
+                logger.warn(TIMELINE_WARN_LOG + "Detected two identical blockingStates events for blockableId = " + o1.getBlockedId() +
+                            ", type = " + o1.getType() + ", ");
+                // Underministic-- not sure that will ever happen. Once we are confident this never happens we should thrown IllegalException
                 return 0;
             }
         });
@@ -136,26 +144,56 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
             int index = insertFromBlockingEvent(accountTimeZone, allEntitlementUUIDs, result, bs, bs.getEffectiveDate(), newEvents);
             insertAfterIndex(result, newEvents, index);
         }
-        return reOrderSubscriptionEventsOnSameDateByType(result);
+        reOrderSubscriptionEventsOnSameDateByType(result);
+        return result;
     }
 
-    private LinkedList<SubscriptionEvent> reOrderSubscriptionEventsOnSameDateByType(final LinkedList<SubscriptionEvent> events) {
-
-        final LinkedList<SubscriptionEvent> result = new LinkedList<SubscriptionEvent>();
-        for (final SubscriptionEvent e : events) {
-            final DefaultSubscriptionEvent cur = (DefaultSubscriptionEvent) e;
-            final DefaultSubscriptionEvent prev = result.size() > 0 ? (DefaultSubscriptionEvent) result.getLast() : null;
-            // If we already inserted an event for that subscription at that specific time, reorder so it follows enum SubscriptionEventType
-            if (prev != null &&
-                prev.getEffectiveDateTime().compareTo(cur.getEffectiveDateTime()) == 0 &&
-                prev.getEntitlementId().equals(cur.getEntitlementId()) &&
-                prev.getSubscriptionEventType().ordinal() > cur.getSubscriptionEventType().ordinal()) {
-                result.add(result.size() - 1, cur);
-            } else {
-                result.add(cur);
+    //
+    // 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 DefaultSubscriptionEvent cur = (DefaultSubscriptionEvent) events.get(i);
+            final DefaultSubscriptionEvent next = (i < (size - 1)) ? (DefaultSubscriptionEvent) 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 DefaultSubscriptionEvent revCur = (DefaultSubscriptionEvent) events.get(currentIndex);
+                    final DefaultSubscriptionEvent other = (DefaultSubscriptionEvent) events.get(currentIndex - 1);
+                    if (!shouldSwap(revCur, other, false)) {
+                        break;
+                    }
+                    Collections.swap(events, currentIndex, currentIndex - 1);
+                    currentIndex--;
+                }
             }
         }
-        return result;
+    }
+
+
+    private boolean shouldSwap(DefaultSubscriptionEvent cur, DefaultSubscriptionEvent other, boolean isAscending) {
+
+        return (cur.getEffectiveDateTime().compareTo(other.getEffectiveDateTime()) == 0 &&
+                cur.getEntitlementId().equals(other.getEntitlementId()) &&
+                ((isAscending && cur.getSubscriptionEventType().ordinal() > other.getSubscriptionEventType().ordinal()) ||
+                 (!isAscending && cur.getSubscriptionEventType().ordinal() < other.getSubscriptionEventType().ordinal())));
     }
 
 
@@ -176,7 +214,11 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         }
     }
 
-    private int insertFromBlockingEvent(final DateTimeZone accountTimeZone, final Set<UUID> allEntitlementUUIDs, final LinkedList<SubscriptionEvent> result, final BlockingState bs, final DateTime bsEffectiveDate, final List<SubscriptionEvent> 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
@@ -241,6 +283,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         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
@@ -274,6 +317,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         return result;
     }
 
+    // Compute the initial stream of events based on the subscription base events
     private LinkedList<SubscriptionEvent> computeSubscriptionBaseEvents(final List<Entitlement> entitlements, final DateTimeZone accountTimeZone) {
         final LinkedList<SubscriptionEvent> result = new LinkedList<SubscriptionEvent>();
         for (final Entitlement cur : entitlements) {
@@ -293,6 +337,9 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
 
     //
     // Old version of code would use CANCEL/RE_CREATE to simulate PAUSE_BILLING/RESUME_BILLING
+    // (Relies on the assumption that there is no blocking_state event matching that CACNEL event so:
+    // 1. The STOP_BILLING (coming from the row CANCEL event) should be transformed into a PAUSE_BILLING
+    // 2. We also add a PAUSE_ENTITLEMENT at the same time as the PAUSE_BILLING
     //
     private void sanitizeForBaseRecreateEvents(final LinkedList<SubscriptionEvent> input) {
         final Collection<UUID> guiltyEntitlementIds = new TreeSet<UUID>();
@@ -327,19 +374,19 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
 
                 int compUUID = event.getEntitlementId().compareTo(cur.getEntitlementId());
                 if (compUUID < 0) {
-                    // Same EffectiveDate but then order by subscriptionId;
+                    // Same EffectiveDate but subscription are different, no need top sort further just return something deterministic
                     break;
                 } else if (compUUID == 0) {
 
                     int eventOrder = event.getSubscriptionEventType().ordinal() - cur.getSubscriptionEventType().ordinal();
                     if (eventOrder < 0) {
-                        // Same EffectiveDate but same subscription, order by eventId;
+                        // Same EffectiveDate and same subscription, order by SubscriptionEventType;
                         break;
                     }
 
                     // Two identical event for the same subscription at the same time, this sounds like some data issue
                     if (eventOrder == 0) {
-                        logger.warn("Detected identical events type = "  + event.getSubscriptionEventType() + " ids = " +
+                        logger.warn(TIMELINE_WARN_LOG + "Detected identical events type = " + event.getSubscriptionEventType() + " ids = " +
                                     event.getId() + ", " + cur.getId() + " for subscription " + cur.getEntitlementId());
                         break;
                     }
@@ -350,6 +397,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         result.add(index, event);
     }
 
+
     private SubscriptionEvent toSubscriptionEvent(final SubscriptionEvent prev, final SubscriptionEvent next, final UUID entitlementId, final BlockingState in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
         return new DefaultSubscriptionEvent(in.getId(),
                                             entitlementId,
@@ -433,7 +481,9 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                 return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.STOP_BILLING);
             case PHASE:
                 return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.PHASE);
-            // STEPH This is the old way of pausing billing; not used any longer, but kept for compatibility reason
+            // This is the old way of pausing billing; not used any longer, but kept for compatibility reason. We return both RESUME_ENTITLEMENT and RESUME_BILLING
+            // and will rely on the sanitizeForBaseRecreateEvents method to transform the STOP_BILLING (coming from CANCEL) into the correct events.
+            //
             case RE_CREATE:
                 return ImmutableList.<SubscriptionEventType>of(SubscriptionEventType.RESUME_ENTITLEMENT, SubscriptionEventType.RESUME_BILLING);
             /*
@@ -469,408 +519,415 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         return events;
     }
 
+//
+// Internal class to keep the state associated with each subscription
+//
+private final static class TargetState {
+
+    private boolean isEntitlementStarted;
+    private boolean isEntitlementStopped;
+    private boolean isBillingStarted;
+    private boolean isBillingStopped;
+    private Map<String, BlockingState> perServiceBlockingState;
+
+    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 BlockingState converted = new DefaultBlockingState(e.getEntitlementId(), BlockingStateType.SUBSCRIPTION,
+                                                                 e.getServiceStateName(), e.getServiceName(), false, e.isBlockedEntitlement(), e.isBlockedBilling(),
+                                                                 ((DefaultSubscriptionEvent) e).getEffectiveDateTime());
+        perServiceBlockingState.put(converted.getService(), converted);
+
+    }
+
     //
-    // Internal class to keep the state associated with each subscription
+    // From the current state of that subscription, compute the effect of the new state based on the incoming blockingState event
     //
-    private final static class TargetState {
+    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;
+        }
 
-        private boolean isEntitlementStarted;
-        private boolean isEntitlementStopped;
-        private boolean isBillingStarted;
-        private boolean isBillingStopped;
-        private Map<String, BlockingState> perServiceBlockingState;
+        //
+        // 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();
+        perServiceBlockingState.put(fixedBlockingState.getService(), fixedBlockingState);
+        final BlockingAggregator stateAfter = getState();
 
-        public TargetState() {
-            this.isEntitlementStarted = false;
-            this.isEntitlementStopped = false;
-            this.isBillingStarted = false;
-            this.isBillingStopped = false;
-            this.perServiceBlockingState = new HashMap<String, BlockingState>();
+        final boolean shouldResumeEntitlement = isEntitlementStarted && !isEntitlementStopped && stateBefore.isBlockEntitlement() && !stateAfter.isBlockEntitlement();
+        if (shouldResumeEntitlement) {
+            result.add(SubscriptionEventType.RESUME_ENTITLEMENT);
         }
-
-        public void setEntitlementStarted() {
-            isEntitlementStarted = true;
+        final boolean shouldResumeBilling = isBillingStarted && !isBillingStopped && stateBefore.isBlockBilling() && !stateAfter.isBlockBilling();
+        if (shouldResumeBilling) {
+            result.add(SubscriptionEventType.RESUME_BILLING);
         }
 
-        public void setEntitlementStopped() {
-            isEntitlementStopped = true;
+        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);
         }
 
-        public void setBillingStarted() {
-            isBillingStarted = true;
+        if (!shouldResumeEntitlement && !shouldBlockEntitlement && !shouldBlockEntitlement && !shouldBlockBilling && !fixedBlockingState.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
+            result.add(SubscriptionEventType.SERVICE_STATE_CHANGE);
         }
+        return result;
+    }
 
-        public void setBillingStopped() {
-            isBillingStopped = true;
+    private BlockingAggregator getState() {
+        final DefaultBlockingAggregator aggrBefore = new DefaultBlockingAggregator();
+        for (BlockingState cur : perServiceBlockingState.values()) {
+            aggrBefore.or(cur);
         }
+        return aggrBefore;
+    }
+}
 
-        public void addEntitlementEvent(final SubscriptionEvent e) {
-            final BlockingState converted = new DefaultBlockingState(e.getEntitlementId(), BlockingStateType.SUBSCRIPTION,
-                                                                     e.getServiceStateName(), e.getServiceName(), false, e.isBlockedEntitlement(), e.isBlockedBilling(),
-                                                                     ((DefaultSubscriptionEvent) e).getEffectiveDateTime());
-            perServiceBlockingState.put(converted.getService(), converted);
 
-        }
+protected static final class DefaultSubscriptionEvent implements SubscriptionEvent {
+
+    private final UUID id;
+    private final UUID entitlementId;
+    private final DateTime effectiveDate;
+    private final DateTime requestedDate;
+    private final SubscriptionEventType eventType;
+    private final boolean isBlockingEntitlement;
+    private final boolean isBlockingBilling;
+    private final String serviceName;
+    private final String serviceStateName;
+    private final Product prevProduct;
+    private final Plan prevPlan;
+    private final PlanPhase prevPlanPhase;
+    private final PriceList prevPriceList;
+    private final BillingPeriod prevBillingPeriod;
+    private final Product nextProduct;
+    private final Plan nextPlan;
+    private final PlanPhase nextPlanPhase;
+    private final PriceList nextPriceList;
+    private final BillingPeriod nextBillingPeriod;
+    private final DateTime createdDate;
+    private final DateTimeZone accountTimeZone;
+
+
+    public DefaultSubscriptionEvent(final UUID id,
+                                     final UUID entitlementId,
+                                     final DateTime effectiveDate,
+                                     final DateTime requestedDate,
+                                     final SubscriptionEventType eventType,
+                                     final boolean blockingEntitlement,
+                                     final boolean blockingBilling,
+                                     final String serviceName,
+                                     final String serviceStateName,
+                                     final Product prevProduct,
+                                     final Plan prevPlan,
+                                     final PlanPhase prevPlanPhase,
+                                     final PriceList prevPriceList,
+                                     final BillingPeriod prevBillingPeriod,
+                                     final Product nextProduct,
+                                     final Plan nextPlan,
+                                     final PlanPhase nextPlanPhase,
+                                     final PriceList nextPriceList,
+                                     final BillingPeriod nextBillingPeriod,
+                                     final DateTime createDate,
+                                     final DateTimeZone accountTimeZone) {
+        this.id = id;
+        this.entitlementId = entitlementId;
+        this.effectiveDate = effectiveDate;
+        this.requestedDate = requestedDate;
+        this.eventType = eventType;
+        this.isBlockingEntitlement = blockingEntitlement;
+        this.isBlockingBilling = blockingBilling;
+        this.serviceName = serviceName;
+        this.serviceStateName = serviceStateName;
+        this.prevProduct = prevProduct;
+        this.prevPlan = prevPlan;
+        this.prevPlanPhase = prevPlanPhase;
+        this.prevPriceList = prevPriceList;
+        this.prevBillingPeriod = prevBillingPeriod;
+        this.nextProduct = nextProduct;
+        this.nextPlan = nextPlan;
+        this.nextPlanPhase = nextPlanPhase;
+        this.nextPriceList = nextPriceList;
+        this.nextBillingPeriod = nextBillingPeriod;
+        this.createdDate = createDate;
+        this.accountTimeZone = accountTimeZone;
+    }
 
-        public List<SubscriptionEventType> addStateAndReturnEventTypes(final BlockingState bs) {
+    private DefaultSubscriptionEvent(DefaultSubscriptionEvent copy, SubscriptionEventType newEventType) {
+        this(copy.getId(),
+             copy.getEntitlementId(),
+             copy.getEffectiveDateTime(),
+             copy.getRequestedDateTime(),
+             newEventType,
+             copy.isBlockedEntitlement(),
+             copy.isBlockedBilling(),
+             copy.getServiceName(),
+             copy.getServiceStateName(),
+             copy.getPrevProduct(),
+             copy.getPrevPlan(),
+             copy.getPrevPhase(),
+             copy.getPrevPriceList(),
+             copy.getPrevBillingPeriod(),
+             copy.getNextProduct(),
+             copy.getNextPlan(),
+             copy.getNextPhase(),
+             copy.getNextPriceList(),
+             copy.getNextBillingPeriod(),
+             copy.getCreatedDate(),
+             copy.getAccountTimeZone());
+    }
 
-            // 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());
+    public DateTimeZone getAccountTimeZone() {
+        return accountTimeZone;
+    }
 
-            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;
-            }
+    public DateTime getEffectiveDateTime() {
+        return effectiveDate;
+    }
 
-            final BlockingAggregator stateBefore = getState();
-            perServiceBlockingState.put(fixedBlockingState.getService(), fixedBlockingState);
-            final BlockingAggregator stateAfter = getState();
+    public DateTime getRequestedDateTime() {
+        return requestedDate;
+    }
 
-            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);
-            }
+    @Override
+    public UUID getId() {
+        return id;
+    }
 
-            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);
-            }
+    @Override
+    public UUID getEntitlementId() {
+        return entitlementId;
+    }
 
-            if (!shouldResumeEntitlement && !shouldBlockEntitlement && !shouldBlockEntitlement && !shouldBlockBilling && !fixedBlockingState.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
-                result.add(SubscriptionEventType.SERVICE_STATE_CHANGE);
-            }
-            return result;
-        }
+    @Override
+    public LocalDate getEffectiveDate() {
+        return effectiveDate != null ? new LocalDate(effectiveDate, accountTimeZone) : null;
+    }
 
-        private BlockingAggregator getState() {
-            final DefaultBlockingAggregator aggrBefore = new DefaultBlockingAggregator();
-            for (BlockingState cur : perServiceBlockingState.values()) {
-                aggrBefore.or(cur);
-            }
-            return aggrBefore;
-        }
-    }
-
-
-    private static final class DefaultSubscriptionEvent implements SubscriptionEvent {
-
-        private final UUID id;
-        private final UUID entitlementId;
-        private final DateTime effectiveDate;
-        private final DateTime requestedDate;
-        private final SubscriptionEventType eventType;
-        private final boolean isBlockingEntitlement;
-        private final boolean isBlockingBilling;
-        private final String serviceName;
-        private final String serviceStateName;
-        private final Product prevProduct;
-        private final Plan prevPlan;
-        private final PlanPhase prevPlanPhase;
-        private final PriceList prevPriceList;
-        private final BillingPeriod prevBillingPeriod;
-        private final Product nextProduct;
-        private final Plan nextPlan;
-        private final PlanPhase nextPlanPhase;
-        private final PriceList nextPriceList;
-        private final BillingPeriod nextBillingPeriod;
-        private final DateTime createdDate;
-        private final DateTimeZone accountTimeZone;
-
-
-        private DefaultSubscriptionEvent(final UUID id,
-                                         final UUID entitlementId,
-                                         final DateTime effectiveDate,
-                                         final DateTime requestedDate,
-                                         final SubscriptionEventType eventType,
-                                         final boolean blockingEntitlement,
-                                         final boolean blockingBilling,
-                                         final String serviceName,
-                                         final String serviceStateName,
-                                         final Product prevProduct,
-                                         final Plan prevPlan,
-                                         final PlanPhase prevPlanPhase,
-                                         final PriceList prevPriceList,
-                                         final BillingPeriod prevBillingPeriod,
-                                         final Product nextProduct,
-                                         final Plan nextPlan,
-                                         final PlanPhase nextPlanPhase,
-                                         final PriceList nextPriceList,
-                                         final BillingPeriod nextBillingPeriod,
-                                         final DateTime createDate,
-                                         final DateTimeZone accountTimeZone) {
-            this.id = id;
-            this.entitlementId = entitlementId;
-            this.effectiveDate = effectiveDate;
-            this.requestedDate = requestedDate;
-            this.eventType = eventType;
-            this.isBlockingEntitlement = blockingEntitlement;
-            this.isBlockingBilling = blockingBilling;
-            this.serviceName = serviceName;
-            this.serviceStateName = serviceStateName;
-            this.prevProduct = prevProduct;
-            this.prevPlan = prevPlan;
-            this.prevPlanPhase = prevPlanPhase;
-            this.prevPriceList = prevPriceList;
-            this.prevBillingPeriod = prevBillingPeriod;
-            this.nextProduct = nextProduct;
-            this.nextPlan = nextPlan;
-            this.nextPlanPhase = nextPlanPhase;
-            this.nextPriceList = nextPriceList;
-            this.nextBillingPeriod = nextBillingPeriod;
-            this.createdDate = createDate;
-            this.accountTimeZone = accountTimeZone;
-        }
-
-        private DefaultSubscriptionEvent(DefaultSubscriptionEvent copy, SubscriptionEventType newEventType) {
-            this(copy.getId(),
-                 copy.getEntitlementId(),
-                 copy.getEffectiveDateTime(),
-                 copy.getRequestedDateTime(),
-                 newEventType,
-                 copy.isBlockedEntitlement(),
-                 copy.isBlockedBilling(),
-                 copy.getServiceName(),
-                 copy.getServiceStateName(),
-                 copy.getPrevProduct(),
-                 copy.getPrevPlan(),
-                 copy.getPrevPhase(),
-                 copy.getPrevPriceList(),
-                 copy.getPrevBillingPeriod(),
-                 copy.getNextProduct(),
-                 copy.getNextPlan(),
-                 copy.getNextPhase(),
-                 copy.getNextPriceList(),
-                 copy.getNextBillingPeriod(),
-                 copy.getCreatedDate(),
-                 copy.getAccountTimeZone());
-        }
+    @Override
+    public LocalDate getRequestedDate() {
+        return requestedDate != null ? new LocalDate(requestedDate, accountTimeZone) : null;
+    }
 
-        public DateTimeZone getAccountTimeZone() {
-            return accountTimeZone;
-        }
+    @Override
+    public SubscriptionEventType getSubscriptionEventType() {
+        return eventType;
+    }
 
-        public DateTime getEffectiveDateTime() {
-            return effectiveDate;
-        }
+    @Override
+    public boolean isBlockedBilling() {
+        return isBlockingBilling;
+    }
 
-        public DateTime getRequestedDateTime() {
-            return requestedDate;
-        }
+    @Override
+    public boolean isBlockedEntitlement() {
+        return isBlockingEntitlement;
+    }
 
-        @Override
-        public UUID getId() {
-            return id;
-        }
+    @Override
+    public String getServiceName() {
+        return serviceName;
+    }
 
-        @Override
-        public UUID getEntitlementId() {
-            return entitlementId;
-        }
+    @Override
+    public String getServiceStateName() {
+        return serviceStateName;
+    }
 
-        @Override
-        public LocalDate getEffectiveDate() {
-            return effectiveDate != null ? new LocalDate(effectiveDate, accountTimeZone) : null;
-        }
+    @Override
+    public Product getPrevProduct() {
+        return prevProduct;
+    }
 
-        @Override
-        public LocalDate getRequestedDate() {
-            return requestedDate != null ? new LocalDate(requestedDate, accountTimeZone) : null;
-        }
+    @Override
+    public Plan getPrevPlan() {
+        return prevPlan;
+    }
 
-        @Override
-        public SubscriptionEventType getSubscriptionEventType() {
-            return eventType;
-        }
+    @Override
+    public PlanPhase getPrevPhase() {
+        return prevPlanPhase;
+    }
 
-        @Override
-        public boolean isBlockedBilling() {
-            return isBlockingBilling;
-        }
+    @Override
+    public PriceList getPrevPriceList() {
+        return prevPriceList;
+    }
 
-        @Override
-        public boolean isBlockedEntitlement() {
-            return isBlockingEntitlement;
-        }
+    @Override
+    public BillingPeriod getPrevBillingPeriod() {
+        return prevBillingPeriod;
+    }
 
-        @Override
-        public String getServiceName() {
-            return serviceName;
-        }
+    @Override
+    public Product getNextProduct() {
+        return nextProduct;
+    }
 
-        @Override
-        public String getServiceStateName() {
-            return serviceStateName;
-        }
+    @Override
+    public Plan getNextPlan() {
+        return nextPlan;
+    }
 
-        @Override
-        public Product getPrevProduct() {
-            return prevProduct;
-        }
+    @Override
+    public PlanPhase getNextPhase() {
+        return nextPlanPhase;
+    }
 
-        @Override
-        public Plan getPrevPlan() {
-            return prevPlan;
-        }
+    @Override
+    public PriceList getNextPriceList() {
+        return nextPriceList;
+    }
 
-        @Override
-        public PlanPhase getPrevPhase() {
-            return prevPlanPhase;
-        }
+    @Override
+    public BillingPeriod getNextBillingPeriod() {
+        return nextBillingPeriod;
+    }
 
-        @Override
-        public PriceList getPrevPriceList() {
-            return prevPriceList;
-        }
+    public DateTime getCreatedDate() {
+        return createdDate;
+    }
 
-        @Override
-        public BillingPeriod getPrevBillingPeriod() {
-            return prevBillingPeriod;
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
         }
-
-        @Override
-        public Product getNextProduct() {
-            return nextProduct;
+        if (o == null || getClass() != o.getClass()) {
+            return false;
         }
 
-        @Override
-        public Plan getNextPlan() {
-            return nextPlan;
-        }
+        final DefaultSubscriptionEvent that = (DefaultSubscriptionEvent) o;
 
-        @Override
-        public PlanPhase getNextPhase() {
-            return nextPlanPhase;
+        if (isBlockingBilling != that.isBlockingBilling) {
+            return false;
         }
-
-        @Override
-        public PriceList getNextPriceList() {
-            return nextPriceList;
+        if (isBlockingEntitlement != that.isBlockingEntitlement) {
+            return false;
         }
-
-        @Override
-        public BillingPeriod getNextBillingPeriod() {
-            return nextBillingPeriod;
+        if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) {
+            return false;
         }
-
-        public DateTime getCreatedDate() {
-            return createdDate;
+        if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+            return false;
         }
-
-        @Override
-        public boolean equals(final Object o) {
-            if (this == o) {
-                return true;
-            }
-            if (o == null || getClass() != o.getClass()) {
-                return false;
-            }
-
-            final DefaultSubscriptionEvent that = (DefaultSubscriptionEvent) o;
-
-            if (isBlockingBilling != that.isBlockingBilling) {
-                return false;
-            }
-            if (isBlockingEntitlement != that.isBlockingEntitlement) {
-                return false;
-            }
-            if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) {
-                return false;
-            }
-            if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
-                return false;
-            }
-            if (entitlementId != null ? !entitlementId.equals(that.entitlementId) : that.entitlementId != null) {
-                return false;
-            }
-            if (eventType != that.eventType) {
-                return false;
-            }
-            if (id != null ? !id.equals(that.id) : that.id != null) {
-                return false;
-            }
-            if (nextBillingPeriod != that.nextBillingPeriod) {
-                return false;
-            }
-            if (nextPlan != null ? !nextPlan.equals(that.nextPlan) : that.nextPlan != null) {
-                return false;
-            }
-            if (nextPlanPhase != null ? !nextPlanPhase.equals(that.nextPlanPhase) : that.nextPlanPhase != null) {
-                return false;
-            }
-            if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
-                return false;
-            }
-            if (nextProduct != null ? !nextProduct.equals(that.nextProduct) : that.nextProduct != null) {
-                return false;
-            }
-            if (prevBillingPeriod != that.prevBillingPeriod) {
-                return false;
-            }
-            if (prevPlan != null ? !prevPlan.equals(that.prevPlan) : that.prevPlan != null) {
-                return false;
-            }
-            if (prevPlanPhase != null ? !prevPlanPhase.equals(that.prevPlanPhase) : that.prevPlanPhase != null) {
-                return false;
-            }
-            if (prevPriceList != null ? !prevPriceList.equals(that.prevPriceList) : that.prevPriceList != null) {
-                return false;
-            }
-            if (prevProduct != null ? !prevProduct.equals(that.prevProduct) : that.prevProduct != null) {
-                return false;
-            }
-            if (requestedDate != null ? !requestedDate.equals(that.requestedDate) : that.requestedDate != null) {
-                return false;
-            }
-            if (serviceName != null ? !serviceName.equals(that.serviceName) : that.serviceName != null) {
-                return false;
-            }
-            if (serviceStateName != null ? !serviceStateName.equals(that.serviceStateName) : that.serviceStateName != null) {
-                return false;
-            }
-
-            return true;
+        if (entitlementId != null ? !entitlementId.equals(that.entitlementId) : that.entitlementId != null) {
+            return false;
         }
-
-        @Override
-        public int hashCode() {
-            int result = id != null ? id.hashCode() : 0;
-            result = 31 * result + (entitlementId != null ? entitlementId.hashCode() : 0);
-            result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
-            result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
-            result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
-            result = 31 * result + (isBlockingEntitlement ? 1 : 0);
-            result = 31 * result + (isBlockingBilling ? 1 : 0);
-            result = 31 * result + (serviceName != null ? serviceName.hashCode() : 0);
-            result = 31 * result + (serviceStateName != null ? serviceStateName.hashCode() : 0);
-            result = 31 * result + (prevProduct != null ? prevProduct.hashCode() : 0);
-            result = 31 * result + (prevPlan != null ? prevPlan.hashCode() : 0);
-            result = 31 * result + (prevPlanPhase != null ? prevPlanPhase.hashCode() : 0);
-            result = 31 * result + (prevPriceList != null ? prevPriceList.hashCode() : 0);
-            result = 31 * result + (prevBillingPeriod != null ? prevBillingPeriod.hashCode() : 0);
-            result = 31 * result + (nextProduct != null ? nextProduct.hashCode() : 0);
-            result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
-            result = 31 * result + (nextPlanPhase != null ? nextPlanPhase.hashCode() : 0);
-            result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
-            result = 31 * result + (nextBillingPeriod != null ? nextBillingPeriod.hashCode() : 0);
-            result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
-            return result;
+        if (eventType != that.eventType) {
+            return false;
+        }
+        if (id != null ? !id.equals(that.id) : that.id != null) {
+            return false;
+        }
+        if (nextBillingPeriod != that.nextBillingPeriod) {
+            return false;
+        }
+        if (nextPlan != null ? !nextPlan.equals(that.nextPlan) : that.nextPlan != null) {
+            return false;
+        }
+        if (nextPlanPhase != null ? !nextPlanPhase.equals(that.nextPlanPhase) : that.nextPlanPhase != null) {
+            return false;
+        }
+        if (nextPriceList != null ? !nextPriceList.equals(that.nextPriceList) : that.nextPriceList != null) {
+            return false;
+        }
+        if (nextProduct != null ? !nextProduct.equals(that.nextProduct) : that.nextProduct != null) {
+            return false;
+        }
+        if (prevBillingPeriod != that.prevBillingPeriod) {
+            return false;
+        }
+        if (prevPlan != null ? !prevPlan.equals(that.prevPlan) : that.prevPlan != null) {
+            return false;
+        }
+        if (prevPlanPhase != null ? !prevPlanPhase.equals(that.prevPlanPhase) : that.prevPlanPhase != null) {
+            return false;
+        }
+        if (prevPriceList != null ? !prevPriceList.equals(that.prevPriceList) : that.prevPriceList != null) {
+            return false;
         }
+        if (prevProduct != null ? !prevProduct.equals(that.prevProduct) : that.prevProduct != null) {
+            return false;
+        }
+        if (requestedDate != null ? !requestedDate.equals(that.requestedDate) : that.requestedDate != null) {
+            return false;
+        }
+        if (serviceName != null ? !serviceName.equals(that.serviceName) : that.serviceName != null) {
+            return false;
+        }
+        if (serviceStateName != null ? !serviceStateName.equals(that.serviceStateName) : that.serviceStateName != null) {
+            return false;
+        }
+
+        return true;
     }
+
+    @Override
+    public int hashCode() {
+        int result = id != null ? id.hashCode() : 0;
+        result = 31 * result + (entitlementId != null ? entitlementId.hashCode() : 0);
+        result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+        result = 31 * result + (requestedDate != null ? requestedDate.hashCode() : 0);
+        result = 31 * result + (eventType != null ? eventType.hashCode() : 0);
+        result = 31 * result + (isBlockingEntitlement ? 1 : 0);
+        result = 31 * result + (isBlockingBilling ? 1 : 0);
+        result = 31 * result + (serviceName != null ? serviceName.hashCode() : 0);
+        result = 31 * result + (serviceStateName != null ? serviceStateName.hashCode() : 0);
+        result = 31 * result + (prevProduct != null ? prevProduct.hashCode() : 0);
+        result = 31 * result + (prevPlan != null ? prevPlan.hashCode() : 0);
+        result = 31 * result + (prevPlanPhase != null ? prevPlanPhase.hashCode() : 0);
+        result = 31 * result + (prevPriceList != null ? prevPriceList.hashCode() : 0);
+        result = 31 * result + (prevBillingPeriod != null ? prevBillingPeriod.hashCode() : 0);
+        result = 31 * result + (nextProduct != null ? nextProduct.hashCode() : 0);
+        result = 31 * result + (nextPlan != null ? nextPlan.hashCode() : 0);
+        result = 31 * result + (nextPlanPhase != null ? nextPlanPhase.hashCode() : 0);
+        result = 31 * result + (nextPriceList != null ? nextPriceList.hashCode() : 0);
+        result = 31 * result + (nextBillingPeriod != null ? nextBillingPeriod.hashCode() : 0);
+        result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+        return result;
+    }
+}
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
index a8aeb16..7e950d0 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestDefaultSubscriptionBundleTimeline.java
@@ -25,6 +25,7 @@ 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;
 
@@ -55,6 +56,161 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         bundleId = UUID.randomUUID();
     }
 
+
+    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 SubscriptionEvent createEvent(final UUID subscriptionId, final SubscriptionEventType type, final DateTime effectiveDate) {
+            return new DefaultSubscriptionEvent(UUID.randomUUID(),
+                                         subscriptionId,
+                                         effectiveDate,
+                                         null,
+                                         type,
+                                         true,
+                                         true,
+                                         "foo",
+                                         "bar",
+                                         null,
+                                         null,
+                                         null,
+                                         null,
+                                         null,
+                                         null,
+                                         null,
+                                         null,
+                                         null,
+                                         null,
+                                         null,
+                                         null);
+
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testReOrderSubscriptionEventsOnInvalidOrder1() {
+        TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+
+        final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
+        final UUID subscriptionId = UUID.randomUUID();
+        final DateTime effectiveDate = clock.getUTCNow();
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_BILLING, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_ENTITLEMENT, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
+
+        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+
+        Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+        Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+        Assert.assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+        Assert.assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
+    }
+
+
+    @Test(groups = "fast")
+    public void testReOrderSubscriptionEventsOnInvalidOrder2() {
+        TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+
+        final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
+        final UUID subscriptionId = UUID.randomUUID();
+        final DateTime effectiveDate = clock.getUTCNow();
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_BILLING, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_ENTITLEMENT, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
+
+        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+
+        Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+        Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+        Assert.assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+        Assert.assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
+    }
+
+    @Test(groups = "fast")
+    public void testReOrderSubscriptionEventsOnInvalidOrder3() {
+        TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+
+        final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
+        final UUID subscriptionId = UUID.randomUUID();
+        final DateTime effectiveDate = clock.getUTCNow();
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_BILLING, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_ENTITLEMENT, effectiveDate));
+
+        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+
+        Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+        Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+        Assert.assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+        Assert.assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
+    }
+
+
+    @Test(groups = "fast")
+    public void testReOrderSubscriptionEventsOnInvalidOrderAndDifferentSubscriptionsDates() {
+        TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+
+        final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
+        final UUID subscriptionId = UUID.randomUUID();
+
+        final UUID otherSubscriptionId = UUID.randomUUID();
+        final DateTime effectiveDate = clock.getUTCNow();
+        final DateTime otherEffectiveDate = clock.getUTCNow().plusDays(1);
+
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_BILLING, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_ENTITLEMENT, effectiveDate));
+
+        events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.START_BILLING, otherEffectiveDate));
+        events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.START_ENTITLEMENT, otherEffectiveDate));
+        events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, otherEffectiveDate));
+        events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.STOP_BILLING, otherEffectiveDate));
+        events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.PAUSE_ENTITLEMENT, otherEffectiveDate));
+        events.add(timeline.createEvent(otherSubscriptionId, SubscriptionEventType.PAUSE_BILLING, otherEffectiveDate));
+
+        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+
+        Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+        Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+        Assert.assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+        Assert.assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
+
+        Assert.assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+        Assert.assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+        Assert.assertEquals(events.get(6).getSubscriptionEventType(), SubscriptionEventType.PAUSE_ENTITLEMENT);
+        Assert.assertEquals(events.get(7).getSubscriptionEventType(), SubscriptionEventType.PAUSE_BILLING);
+        Assert.assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+        Assert.assertEquals(events.get(9).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
+
+
+    }
+
+    @Test(groups = "fast")
+    public void testReOrderSubscriptionEventsOnCorrectOrder() {
+        TestSubscriptionBundleTimeline timeline = new TestSubscriptionBundleTimeline(null, null, null, null, new ArrayList<Entitlement>(), new ArrayList<BlockingState>());
+
+        final List<SubscriptionEvent> events = new ArrayList<SubscriptionEvent>();
+        final UUID subscriptionId = UUID.randomUUID();
+        final DateTime effectiveDate = clock.getUTCNow();
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_ENTITLEMENT, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.START_BILLING, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_ENTITLEMENT, effectiveDate));
+        events.add(timeline.createEvent(subscriptionId, SubscriptionEventType.STOP_BILLING, effectiveDate));
+
+        timeline.reOrderSubscriptionEventsOnSameDateByType(events);
+
+        Assert.assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+        Assert.assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+        Assert.assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+        Assert.assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
+    }
+
     @Test(groups = "fast")
     public void testOneEntitlementNoBlockingStates() throws CatalogApiException {