killbill-memoizeit

entitlement: fix ordering in specific scenarii This closes

12/13/2013 4:26:00 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 c182b4c..99af794 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
@@ -215,16 +215,67 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
         }
     }
 
+    private int 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 {
+            // Respect enum ordering
+            return ((Integer) first.getSubscriptionEventType().ordinal()).compareTo(second.getSubscriptionEventType().ordinal());
+        }
+    }
+
     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());
         return (cur.getEffectiveDate().compareTo(other.getEffectiveDate()) == 0 &&
                 ((isAscending &&
                   ((idComp > 0) ||
-                   (idComp == 0 && cur.getSubscriptionEventType().ordinal() > other.getSubscriptionEventType().ordinal()))) ||
+                   (idComp == 0 && compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(cur, other) > 0))) ||
                  (!isAscending &&
                   ((idComp < 0) ||
-                   (idComp == 0 && cur.getSubscriptionEventType().ordinal() < other.getSubscriptionEventType().ordinal())))));
+                   (idComp == 0 && compareSubscriptionEventsForSameEffectiveDateAndEntitlementId(cur, other) < 0)))));
     }
 
     private void insertAfterIndex(final LinkedList<SubscriptionEvent> original, final List<SubscriptionEvent> newEvents, final int index) {
@@ -297,13 +348,13 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                                                 ImmutableList.<UUID>copyOf(allEntitlementUUIDs);
 
         // For each target compute the new events that should be inserted in the stream
-        for (final UUID target : targetEntitlementIds) {
-            final SubscriptionEvent[] prevNext = findPrevNext(result, target, curInsertion);
-            final TargetState curTargetState = targetStates.get(target);
+        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], target, bs, t, accountTimeZone));
+                newEvents.add(toSubscriptionEvent(prevNext[0], prevNext[1], targetEntitlementId, bs, t, accountTimeZone));
             }
         }
         return index;
@@ -333,7 +384,9 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
                     break;
                 }
             }
-            if (tmp.getId().equals(insertionEvent.getId())) {
+            // Check both the id and the event type because of multiplexing
+            if (tmp.getId().equals(insertionEvent.getId()) &&
+                tmp.getSubscriptionEventType().equals(insertionEvent.getSubscriptionEventType())) {
                 foundCur = true;
             }
         }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionEvent.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionEvent.java
index fbd7ab6..dbeb9c1 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionEvent.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscriptionEvent.java
@@ -96,30 +96,6 @@ public class DefaultSubscriptionEvent implements SubscriptionEvent {
         this.accountTimeZone = accountTimeZone;
     }
 
-    public DefaultSubscriptionEvent(final DefaultSubscriptionEvent copy, final SubscriptionEventType newEventType) {
-        this(copy.getId(),
-             copy.getEntitlementId(),
-             copy.getEffectiveDateTime(),
-             copy.getRequestedDateTime(),
-             newEventType,
-             copy.isBlockedEntitlement(),
-             copy.isBlockedBilling(),
-             copy.getServiceName(),
-             copy.getServiceStateName(),
-             copy.getPrevProduct(),
-             copy.getPrevPlan(),
-             copy.getPrevPhase(),
-             copy.getPrevPriceList(),
-             copy.getPrevBillingPeriod(),
-             copy.getNextProduct(),
-             copy.getNextPlan(),
-             copy.getNextPhase(),
-             copy.getNextPriceList(),
-             copy.getNextBillingPeriod(),
-             copy.getCreatedDate(),
-             copy.getAccountTimeZone());
-    }
-
     public DateTimeZone getAccountTimeZone() {
         return accountTimeZone;
     }
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 fd225ad..9932e3f 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
@@ -200,7 +200,6 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         Assert.assertEquals(events.get(3).getEntitlementId(), subscriptionId);
         Assert.assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
         Assert.assertEquals(events.get(4).getEntitlementId(), otherSubscriptionId);
-
     }
 
     @Test(groups = "fast")
@@ -455,6 +454,155 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(8).getNextPhase().getName(), "phase");
     }
 
+    @Test(groups = "fast", description = "Test for https://github.com/killbill/killbill/issues/147 and https://github.com/killbill/killbill/issues/148")
+    public void testOneEntitlementWithOverduePauseThenCancel() throws CatalogApiException {
+        clock.setDay(new LocalDate(2013, 1, 1));
+
+        final DateTimeZone accountTimeZone = DateTimeZone.UTC;
+        final UUID accountId = UUID.randomUUID();
+        final UUID bundleId = UUID.randomUUID();
+        final String externalKey = "foo";
+
+        final UUID entitlementId = UUID.randomUUID();
+
+        final List<SubscriptionBaseTransition> allTransitions = new ArrayList<SubscriptionBaseTransition>();
+        final List<BlockingState> blockingStates = new ArrayList<BlockingState>();
+
+        final DateTime requestedDate = new DateTime();
+        DateTime effectiveDate = new DateTime(2013, 1, 1, 15, 43, 25, 0, DateTimeZone.UTC);
+        final SubscriptionBaseTransition tr1 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CREATE, requestedDate, effectiveDate, clock.getUTCNow(), null, "trial");
+        allTransitions.add(tr1);
+
+        effectiveDate = effectiveDate.plusDays(30);
+        clock.addDays(30);
+        final SubscriptionBaseTransition tr2 = createTransition(entitlementId, EventType.PHASE, null, requestedDate, effectiveDate, clock.getUTCNow(), "trial", "phase");
+        allTransitions.add(tr2);
+
+        final String overdueService = "overdue-service";
+        effectiveDate = effectiveDate.plusDays(12);
+        clock.addDays(12);
+        final BlockingState bs1 = new DefaultBlockingState(UUID.randomUUID(), accountId, BlockingStateType.ACCOUNT,
+                                                           "ODE1", overdueService,
+                                                           true, false, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+        blockingStates.add(bs1);
+
+        effectiveDate = effectiveDate.plusDays(42);
+        clock.addDays(42);
+        final BlockingState bs2 = new DefaultBlockingState(UUID.randomUUID(), accountId, BlockingStateType.ACCOUNT,
+                                                           "ODE2", overdueService,
+                                                           true, false, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+        blockingStates.add(bs2);
+
+        effectiveDate = effectiveDate.plusDays(15);
+        clock.addDays(15);
+        final BlockingState bs3 = new DefaultBlockingState(UUID.randomUUID(), accountId, BlockingStateType.ACCOUNT,
+                                                           "ODE3", overdueService,
+                                                           true, true, true, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+        blockingStates.add(bs3);
+
+        effectiveDate = effectiveDate.plusDays(15);
+        clock.addDays(15);
+        final BlockingState bs4 = new DefaultBlockingState(UUID.randomUUID(), entitlementId, BlockingStateType.SUBSCRIPTION,
+                                                           DefaultEntitlementApi.ENT_STATE_CANCELLED, DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+                                                           true, true, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+        blockingStates.add(bs4);
+
+        effectiveDate = effectiveDate.plusDays(1);
+        clock.addDays(1);
+        final BlockingState bs5 = new DefaultBlockingState(UUID.randomUUID(), accountId, BlockingStateType.ACCOUNT,
+                                                           "ODE4", overdueService,
+                                                           true, true, true, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+        blockingStates.add(bs5);
+        // Note: cancellation event and ODE4 at the same effective date (see https://github.com/killbill/killbill/issues/148)
+        final SubscriptionBaseTransition tr3 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CANCEL, effectiveDate, effectiveDate, clock.getUTCNow(), "phase", null);
+        allTransitions.add(tr3);
+
+        final List<Entitlement> entitlements = new ArrayList<Entitlement>();
+        final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+        entitlements.add(entitlement);
+
+        final SubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+
+        assertEquals(timeline.getAccountId(), accountId);
+        assertEquals(timeline.getBundleId(), bundleId);
+        assertEquals(timeline.getExternalKey(), externalKey);
+
+        final List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
+        assertEquals(events.size(), 10);
+
+        assertEquals(events.get(0).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
+        assertEquals(events.get(1).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
+        assertEquals(events.get(2).getEffectiveDate().compareTo(new LocalDate(tr2.getEffectiveTransitionTime(), accountTimeZone)), 0);
+        assertEquals(events.get(3).getEffectiveDate().compareTo(new LocalDate(bs1.getEffectiveDate(), accountTimeZone)), 0);
+        assertEquals(events.get(4).getEffectiveDate().compareTo(new LocalDate(bs2.getEffectiveDate(), accountTimeZone)), 0);
+        assertEquals(events.get(5).getEffectiveDate().compareTo(new LocalDate(bs3.getEffectiveDate(), accountTimeZone)), 0);
+        assertEquals(events.get(6).getEffectiveDate().compareTo(new LocalDate(bs3.getEffectiveDate(), accountTimeZone)), 0);
+        assertEquals(events.get(7).getEffectiveDate().compareTo(new LocalDate(bs4.getEffectiveDate(), accountTimeZone)), 0);
+        assertEquals(events.get(8).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
+        assertEquals(events.get(9).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
+
+        assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+        assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+
+        assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.PHASE);
+
+        assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
+        assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
+
+        assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.PAUSE_ENTITLEMENT);
+        assertEquals(events.get(6).getSubscriptionEventType(), SubscriptionEventType.PAUSE_BILLING);
+
+        assertEquals(events.get(7).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+
+        assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
+        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(2).getServiceName(), DefaultSubscriptionBundleTimeline.ENT_BILLING_SERVICE_NAME);
+
+        assertEquals(events.get(3).getServiceName(), overdueService);
+        assertEquals(events.get(4).getServiceName(), overdueService);
+        assertEquals(events.get(5).getServiceName(), overdueService);
+        assertEquals(events.get(6).getServiceName(), overdueService);
+
+        assertEquals(events.get(7).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
+
+        assertEquals(events.get(8).getServiceName(), overdueService);
+        assertEquals(events.get(9).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
+
+        assertNull(events.get(0).getPrevPhase());
+        assertEquals(events.get(0).getNextPhase().getName(), "trial");
+        assertNull(events.get(1).getPrevPhase());
+        assertEquals(events.get(1).getNextPhase().getName(), "trial");
+
+        assertEquals(events.get(2).getPrevPhase().getName(), "trial");
+        assertEquals(events.get(2).getNextPhase().getName(), "phase");
+
+        assertEquals(events.get(3).getPrevPhase().getName(), "phase");
+        assertEquals(events.get(3).getNextPhase().getName(), "phase");
+        assertEquals(events.get(4).getPrevPhase().getName(), "phase");
+        assertEquals(events.get(4).getNextPhase().getName(), "phase");
+        assertEquals(events.get(5).getPrevPhase().getName(), "phase");
+        assertEquals(events.get(5).getNextPhase().getName(), "phase");
+        assertEquals(events.get(6).getPrevPhase().getName(), "phase");
+        assertEquals(events.get(6).getNextPhase().getName(), "phase");
+
+        assertEquals(events.get(7).getPrevPhase().getName(), "phase");
+        assertNull(events.get(7).getNextPhase());
+
+        assertEquals(events.get(8).getPrevPhase().getName(), "phase");
+        assertNull(events.get(8).getNextPhase());
+        assertEquals(events.get(9).getPrevPhase().getName(), "phase");
+        assertNull(events.get(9).getNextPhase());
+    }
+
     @Test(groups = "fast")
     public void testOneEntitlementWithInitialBlockingState() throws CatalogApiException {
         clock.setDay(new LocalDate(2013, 1, 1));
@@ -813,23 +961,23 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
         assertEquals(events.get(0).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
         assertEquals(events.get(1).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
         assertEquals(events.get(2).getEffectiveDate().compareTo(new LocalDate(tr2.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(3).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
+        assertEquals(events.get(3).getEffectiveDate().compareTo(new LocalDate(bs1.getEffectiveDate(), accountTimeZone)), 0);
         assertEquals(events.get(4).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
-        assertEquals(events.get(5).getEffectiveDate().compareTo(new LocalDate(bs1.getEffectiveDate(), accountTimeZone)), 0);
+        assertEquals(events.get(5).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
 
         assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
         assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
         assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.PHASE);
-        assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
-        assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
-        assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
+        assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
+        assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+        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(3).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
-        assertEquals(events.get(4).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
-        assertEquals(events.get(5).getServiceName(), service);
+        assertEquals(events.get(3).getServiceName(), service);
+        assertEquals(events.get(4).getServiceName(), DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME);
+        assertEquals(events.get(5).getServiceName(), DefaultSubscriptionBundleTimeline.BILLING_SERVICE_NAME);
 
         assertNull(events.get(0).getPrevPhase());
         assertEquals(events.get(0).getNextPhase().getName(), "trial");