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 4792c0e..fd409c6 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
@@ -33,6 +33,8 @@ import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Plan;
@@ -42,10 +44,10 @@ import com.ning.billing.catalog.api.Product;
import com.ning.billing.entitlement.DefaultEntitlementService;
import com.ning.billing.entitlement.block.BlockingChecker.BlockingAggregator;
import com.ning.billing.entitlement.block.DefaultBlockingChecker.DefaultBlockingAggregator;
+import com.ning.billing.junction.DefaultBlockingState;
import com.ning.billing.subscription.api.SubscriptionBase;
import com.ning.billing.subscription.api.SubscriptionBaseTransitionType;
import com.ning.billing.subscription.api.user.SubscriptionBaseTransition;
-import com.ning.billing.junction.DefaultBlockingState;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
@@ -53,6 +55,9 @@ import com.google.common.collect.ImmutableList;
public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTimeline {
+
+ private final Logger logger = LoggerFactory.getLogger(DefaultSubscriptionBundleTimeline.class);
+
public static final String BILLING_SERVICE_NAME = "billing-service";
public static final String ENT_BILLING_SERVICE_NAME = "entitlement+billing-service";
@@ -96,23 +101,30 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
if (effectivedComp != 0) {
return effectivedComp;
}
- final int createdDateComp = o1.getCreatedDate().compareTo(o2.getCreatedDate());
- if (createdDateComp != 0) {
- return createdDateComp;
+ // For the same effectiveDate we want to first return ENTITLEMENT events
+ final int serviceNameComp = o1.getService().compareTo(o2.getService());
+ if (serviceNameComp != 0) {
+ if (o1.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
+ return -1;
+ } else if (o2.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
+ return 1;
+ } else {
+ return serviceNameComp;
+ }
}
- final int uuidComp = o1.getId().compareTo(o2.getId());
+ 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
- final int serviceNameComp = o1.getService().compareTo(o2.getService());
- if (serviceNameComp != 0) {
- return serviceNameComp;
- }
final int serviceStateComp = o1.getStateName().compareTo(o2.getStateName());
if (serviceStateComp != 0) {
return serviceStateComp;
}
+ final int createdDateComp = o1.getCreatedDate().compareTo(o2.getCreatedDate());
+ if (createdDateComp != 0) {
+ return createdDateComp;
+ }
// Underministic-- not sure that will ever happen.
return 0;
}
@@ -122,11 +134,48 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
final List<SubscriptionEvent> newEvents = new ArrayList<SubscriptionEvent>();
int index = insertFromBlockingEvent(accountTimeZone, allEntitlementUUIDs, result, bs, bs.getEffectiveDate(), newEvents);
- result.addAll(index, newEvents);
+ insertAfterIndex(result, newEvents, index);
+ }
+ return reOrderSubscriptionEventsOnSameDateByType(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);
+ }
}
return result;
}
+
+ private void insertAfterIndex(final LinkedList<SubscriptionEvent> original, final List<SubscriptionEvent> newEvents, int index) {
+
+ final boolean firstPosition = (index == -1);
+ final boolean lastPosition = (index == original.size() - 1);
+ if (lastPosition || firstPosition) {
+ for (final SubscriptionEvent cur : newEvents) {
+ if (lastPosition) {
+ original.addLast(cur);
+ } else {
+ original.addFirst(cur);
+ }
+ }
+ } else {
+ original.addAll(index + 1, newEvents);
+ }
+ }
+
private int insertFromBlockingEvent(final DateTimeZone accountTimeZone, final Set<UUID> allEntitlementUUIDs, final LinkedList<SubscriptionEvent> result, final BlockingState bs, final DateTime bsEffectiveDate, final List<SubscriptionEvent> newEvents) {
@@ -145,14 +194,12 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
DefaultSubscriptionEvent curInsertion = null;
while (it.hasNext()) {
DefaultSubscriptionEvent cur = (DefaultSubscriptionEvent) it.next();
- index++;
-
final int compEffectiveDate = bsEffectiveDate.compareTo(cur.getEffectiveDateTime());
- final boolean shouldContinue = (compEffectiveDate > 0 ||
- (compEffectiveDate == 0 && bs.getCreatedDate().compareTo(cur.getCreatedDate()) >= 0));
+ final boolean shouldContinue = (compEffectiveDate >= 0);
if (!shouldContinue) {
break;
}
+ index++;
final TargetState curTargetState = targetStates.get(cur.getEntitlementId());
switch (cur.getSubscriptionEventType()) {
@@ -278,20 +325,23 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
break;
} else if (compEffectiveDate == 0) {
- int compCreatedDate = ((DefaultSubscriptionEvent) event).getCreatedDate().compareTo(((DefaultSubscriptionEvent) cur).getCreatedDate());
- if (compCreatedDate < 0) {
- // Same EffectiveDate but CreatedDate is less than cur -> insert here
+ int compUUID = event.getEntitlementId().compareTo(cur.getEntitlementId());
+ if (compUUID < 0) {
+ // Same EffectiveDate but then order by subscriptionId;
break;
- } else if (compCreatedDate == 0) {
- int compUUID = event.getId().compareTo(cur.getId());
- if (compUUID < 0) {
- // Same EffectiveDate and CreatedDate but order by ID
+ } else if (compUUID == 0) {
+
+ int eventOrder = event.getSubscriptionEventType().ordinal() - cur.getSubscriptionEventType().ordinal();
+ if (eventOrder < 0) {
+ // Same EffectiveDate but same subscription, order by eventId;
+ 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 = " +
+ event.getId() + ", " + cur.getId() + " for subscription " + cur.getEntitlementId());
break;
- } else if (compUUID == 0) {
- if (event.getSubscriptionEventType().ordinal() < cur.getSubscriptionEventType().ordinal()) {
- // Same EffectiveDate, CreatedDate and ID, but event type is lower -- as described in enum
- break;
- }
}
}
}
@@ -326,7 +376,6 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
}
-
private SubscriptionEvent toSubscriptionEvent(final SubscriptionBaseTransition in, final SubscriptionEventType eventType, final DateTimeZone accountTimeZone) {
return new DefaultSubscriptionEvent(in.getId(),
in.getSubscriptionId(),
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 05d9adc..a8aeb16 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
@@ -467,6 +467,122 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
}
+
+ @Test(groups = "fast")
+ public void testWithOverdueOfflineAndClear() 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, 23, 11, 8, 0, DateTimeZone.UTC);
+ final SubscriptionBaseTransition tr1 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CREATE, requestedDate, effectiveDate, clock.getUTCNow(), null, "trial");
+ allTransitions.add(tr1);
+
+ effectiveDate = effectiveDate.plusDays(30);
+ clock.addDays(30);
+ final SubscriptionBaseTransition tr2 = createTransition(entitlementId, EventType.PHASE, null, requestedDate, effectiveDate, clock.getUTCNow(), "trial", "phase");
+ allTransitions.add(tr2);
+
+ effectiveDate = effectiveDate.plusDays(6); // 2013-02-06
+ clock.addDays(6);
+ final SubscriptionBaseTransition tr3 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CANCEL, requestedDate, effectiveDate, clock.getUTCNow(), "phase", null);
+ allTransitions.add(tr3);
+
+ effectiveDate = effectiveDate.plusDays(22);// 2013-02-28
+ clock.addDays(22);
+ final SubscriptionBaseTransition tr4 = createTransition(entitlementId, EventType.API_USER, ApiEventType.RE_CREATE, requestedDate, effectiveDate, clock.getUTCNow(), null, "phase");
+ allTransitions.add(tr4);
+
+ effectiveDate = effectiveDate.plusDays(12); // 2013-03-12
+ clock.addDays(12);
+ final SubscriptionBaseTransition tr5 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CANCEL, requestedDate, effectiveDate, clock.getUTCNow(), "phase", null);
+ allTransitions.add(tr5);
+
+ final BlockingState bs1 = new DefaultBlockingState(UUID.randomUUID(), entitlementId, BlockingStateType.ACCOUNT,
+ "OFFLINE", "overdue-service",
+ true, true, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+ blockingStates.add(bs1);
+
+
+ final BlockingState bs2 = 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(bs2);
+
+
+ effectiveDate = effectiveDate.plusDays(12); // 2013-03-24
+ clock.addDays(12);
+
+ final BlockingState bs3 = new DefaultBlockingState(UUID.randomUUID(), entitlementId, BlockingStateType.ACCOUNT,
+ "__KILLBILL__CLEAR__OVERDUE__STATE__", "overdue-service",
+ false, false, false, effectiveDate, clock.getUTCNow(), clock.getUTCNow());
+
+ blockingStates.add(bs3);
+
+
+ final List<Entitlement> entitlements = new ArrayList<Entitlement>();
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ entitlements.add(entitlement);
+
+ 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(), 11);
+
+ assertEquals(events.get(0).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(1).getEffectiveDate().compareTo(new LocalDate(tr1.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(2).getEffectiveDate().compareTo(new LocalDate(tr2.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(3).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(4).getEffectiveDate().compareTo(new LocalDate(tr3.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(5).getEffectiveDate().compareTo(new LocalDate(tr4.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(6).getEffectiveDate().compareTo(new LocalDate(tr4.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(7).getEffectiveDate().compareTo(new LocalDate(tr5.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(8).getEffectiveDate().compareTo(new LocalDate(tr5.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(9).getEffectiveDate().compareTo(new LocalDate(tr5.getEffectiveTransitionTime(), accountTimeZone)), 0);
+ assertEquals(events.get(10).getEffectiveDate().compareTo(new LocalDate(bs3.getEffectiveDate(), accountTimeZone)), 0);
+
+ assertEquals(events.get(0).getSubscriptionEventType(), SubscriptionEventType.START_ENTITLEMENT);
+ assertEquals(events.get(1).getSubscriptionEventType(), SubscriptionEventType.START_BILLING);
+ assertEquals(events.get(2).getSubscriptionEventType(), SubscriptionEventType.PHASE);
+ assertEquals(events.get(3).getSubscriptionEventType(), SubscriptionEventType.PAUSE_ENTITLEMENT);
+ assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.PAUSE_BILLING);
+ assertEquals(events.get(5).getSubscriptionEventType(), SubscriptionEventType.RESUME_ENTITLEMENT);
+ assertEquals(events.get(6).getSubscriptionEventType(), SubscriptionEventType.RESUME_BILLING);
+ assertEquals(events.get(7).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
+ assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
+ assertEquals(events.get(9).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
+ assertEquals(events.get(10).getSubscriptionEventType(), SubscriptionEventType.SERVICE_STATE_CHANGE);
+
+ assertEquals(events.get(0).getNextPhase().getName(), "trial");
+ assertEquals(events.get(1).getNextPhase().getName(), "trial");
+ assertEquals(events.get(2).getNextPhase().getName(), "phase");
+ assertEquals(events.get(3).getNextPhase(), null);
+ assertEquals(events.get(4).getNextPhase(), null);
+ assertEquals(events.get(5).getNextPhase().getName(), "phase");
+ assertEquals(events.get(6).getNextPhase().getName(), "phase");
+ assertEquals(events.get(7).getNextPhase(), null);
+ assertEquals(events.get(8).getNextPhase(), null);
+ assertEquals(events.get(9).getNextPhase(), null);
+ assertEquals(events.get(10).getNextPhase(), null);
+ }
+
+
private DefaultEntitlement createEntitlement(final UUID entitlementId, final List<SubscriptionBaseTransition> allTransitions) {
final DefaultEntitlement result = Mockito.mock(DefaultEntitlement.class);