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/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");