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 2e8ab03..f6343f5 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
@@ -129,7 +129,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
// Keep the current state per entitlement
- final Map<UUID, TargetState> targetStates= new HashMap<UUID, TargetState>();
+ final Map<UUID, TargetState> targetStates = new HashMap<UUID, TargetState>();
for (UUID cur : allEntitlementUUIDs) {
targetStates.put(cur, new TargetState());
}
@@ -140,38 +140,38 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
int index = -1;
final Iterator<SubscriptionEvent> it = result.iterator();
// Where we need to insert in that stream
- DefaultSubscriptionEvent cur = null;
+ DefaultSubscriptionEvent curInsertion = null;
while (it.hasNext()) {
- cur = (DefaultSubscriptionEvent) it.next();
+ DefaultSubscriptionEvent cur = (DefaultSubscriptionEvent) it.next();
index++;
final int compEffectiveDate = bsEffectiveDate.compareTo(cur.getEffectiveDate());
final boolean shouldContinue = (compEffectiveDate > 0 ||
(compEffectiveDate == 0 && bs.getCreatedDate().compareTo(cur.getCreatedDate()) >= 0));
+ if (!shouldContinue) {
+ break;
+ }
final TargetState curTargetState = targetStates.get(cur.getEntitlementId());
- if (shouldContinue) {
- switch (cur.getSubscriptionEventType()) {
- case START_ENTITLEMENT:
- curTargetState.setEntitlementStarted();
- break;
- case STOP_ENTITLEMENT:
- curTargetState.setEntitlementStopped();
- break;
- case START_BILLING:
- curTargetState.setBillingStarted();
- break;
- case PAUSE_BILLING:
- case PAUSE_ENTITLEMENT:
- curTargetState.addEntitlementEvent(cur);
- break;
- case STOP_BILLING:
- curTargetState.setBillingStopped();
- break;
- }
- } else {
- break;
+ switch (cur.getSubscriptionEventType()) {
+ case START_ENTITLEMENT:
+ curTargetState.setEntitlementStarted();
+ break;
+ case STOP_ENTITLEMENT:
+ curTargetState.setEntitlementStopped();
+ break;
+ case START_BILLING:
+ curTargetState.setBillingStarted();
+ break;
+ case PAUSE_BILLING:
+ case PAUSE_ENTITLEMENT:
+ curTargetState.addEntitlementEvent(cur);
+ break;
+ case STOP_BILLING:
+ curTargetState.setBillingStopped();
+ break;
}
+ curInsertion = cur;
}
// Extract the list of targets based on the type of blocking state
@@ -181,7 +181,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
// For each target compute the new events that should be inserted in the stream
for (UUID target : targetEntitlementIds) {
- final SubscriptionEvent[] prevNext = findPrevNext(result, target, cur, bs);
+ final SubscriptionEvent[] prevNext = findPrevNext(result, target, curInsertion);
final TargetState curTargetState = targetStates.get(target);
final List<SubscriptionEventType> eventTypes = curTargetState.addStateAndReturnEventTypes(bs);
@@ -192,10 +192,16 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
return index;
}
- private SubscriptionEvent[] findPrevNext(final List<SubscriptionEvent> events, final UUID targetEntitlementId, final SubscriptionEvent insertionEvent, final BlockingState bs) {
+ private SubscriptionEvent[] findPrevNext(final List<SubscriptionEvent> events, final UUID targetEntitlementId, final SubscriptionEvent insertionEvent) {
// Find prev/next event for the same entitlement
- final SubscriptionEvent[] result = new DefaultSubscriptionEvent[2];
+ final SubscriptionEvent[] result = new DefaultSubscriptionEvent[2];
+ if (insertionEvent == null) {
+ result[0] = null;
+ result[1] = events.size() > 0 ? events.get(0) : null;
+ return result;
+ }
+
final Iterator<SubscriptionEvent> it = events.iterator();
DefaultSubscriptionEvent prev = null;
DefaultSubscriptionEvent next = null;
@@ -251,16 +257,16 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
private void sanitizeForBaseRecreateEvents(final LinkedList<SubscriptionEvent> input) {
final Set<UUID> guiltyEntitlementIds = new TreeSet<UUID>();
- ListIterator<SubscriptionEvent> it = input.listIterator(input.size() - 1);
+ ListIterator<SubscriptionEvent> it = input.listIterator(input.size());
while (it.hasPrevious()) {
final SubscriptionEvent cur = it.previous();
if (cur.getSubscriptionEventType() == SubscriptionEventType.RESUME_BILLING) {
- guiltyEntitlementIds.add(cur.getId());
+ guiltyEntitlementIds.add(cur.getEntitlementId());
continue;
}
if (cur.getSubscriptionEventType() == SubscriptionEventType.STOP_BILLING &&
- guiltyEntitlementIds.contains(cur.getId())) {
- guiltyEntitlementIds.remove(cur.getId());
+ guiltyEntitlementIds.contains(cur.getEntitlementId())) {
+ guiltyEntitlementIds.remove(cur.getEntitlementId());
final SubscriptionEvent correctedEvent = new DefaultSubscriptionEvent((DefaultSubscriptionEvent) cur, SubscriptionEventType.PAUSE_BILLING);
it.set(correctedEvent);
}
@@ -314,7 +320,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
prev != null ? (prev.getNextPlan() != null ? prev.getNextPlan() : prev.getPrevPlan()) : null,
prev != null ? (prev.getNextPhase() != null ? prev.getNextPhase() : prev.getPrevPhase()) : null,
prev != null ? (prev.getNextPriceList() != null ? prev.getNextPriceList() : prev.getPrevPriceList()) : null,
- prev != null ? (prev.getNextBillingPeriod() != null ? prev.getNextBillingPeriod() : prev.getPrevBillingPeriod()) : null,
+ prev != null ? (prev.getNextBillingPeriod() != null ? prev.getNextBillingPeriod() : prev.getPrevBillingPeriod()) : null,
next != null ? next.getPrevProduct() : null,
next != null ? next.getPrevPlan() : null,
next != null ? next.getPrevPhase() : null,
@@ -401,6 +407,9 @@ 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;
@@ -425,7 +434,6 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
isEntitlementStopped = true;
}
-
public void setBillingStarted() {
isBillingStarted = true;
}
@@ -441,17 +449,28 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
perServiceBlockingState.put(converted.getService(), converted);
}
+
public List<SubscriptionEventType> addStateAndReturnEventTypes(final BlockingState bs) {
- final List<SubscriptionEventType> result = new ArrayList<SubscriptionEventType>(4);
- if (bs.getStateName().equals(DefaultEntitlementApi.ENT_STATE_CANCELLED)) {
+ // 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;
}
final BlockingAggregator stateBefore = getState();
- perServiceBlockingState.put(bs.getService(), bs);
+ perServiceBlockingState.put(fixedBlockingState.getService(), fixedBlockingState);
final BlockingAggregator stateAfter = getState();
final boolean shouldResumeEntitlement = isEntitlementStarted && !isEntitlementStopped && stateBefore.isBlockEntitlement() && !stateAfter.isBlockEntitlement();
@@ -472,7 +491,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
result.add(SubscriptionEventType.PAUSE_BILLING);
}
- if (!shouldResumeEntitlement && !shouldBlockEntitlement && !shouldBlockEntitlement && !shouldBlockBilling && !bs.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
+ if (!shouldResumeEntitlement && !shouldBlockEntitlement && !shouldBlockEntitlement && !shouldBlockBilling && !fixedBlockingState.getService().equals(DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME)) {
result.add(SubscriptionEventType.SERVICE_STATE_CHANGE);
}
return result;
@@ -578,7 +597,7 @@ public class DefaultSubscriptionBundleTimeline implements SubscriptionBundleTime
copy.getNextPriceList(),
copy.getNextBillingPeriod(),
copy.getCreatedDate(),
- copy.getAccountTimeZone());
+ copy.getAccountTimeZone());
}
public DateTimeZone getAccountTimeZone() {
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 2eac11a..a76882e 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
@@ -117,6 +117,159 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
}
+
+ @Test(groups = "fast")
+ public void testOneEntitlementWithRecreate() 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 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);
+
+
+ effectiveDate = effectiveDate.plusDays(15);
+ clock.addDays(15);
+ final SubscriptionBaseTransition tr3 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CANCEL, requestedDate, effectiveDate, clock.getUTCNow(), "phase", null);
+ allTransitions.add(tr3);
+
+ effectiveDate = effectiveDate.plusDays(15);
+ clock.addDays(15);
+ final SubscriptionBaseTransition tr4 = createTransition(entitlementId, EventType.API_USER, ApiEventType.RE_CREATE, requestedDate, effectiveDate, clock.getUTCNow(), null, "phase");
+ allTransitions.add(tr4);
+
+
+ final List<Entitlement> entitlements = new ArrayList<Entitlement>();
+ final Entitlement entitlement = createEntitlement(entitlementId, allTransitions);
+ entitlements.add(entitlement);
+
+ final DefaultSubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, Collections.<BlockingState>emptyList());
+
+ assertEquals(timeline.getAccountId(), accountId);
+ assertEquals(timeline.getBundleId(), bundleId);
+ assertEquals(timeline.getExternalKey(), externalKey);
+
+ List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
+ assertEquals(events.size(), 5);
+
+ 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(tr4.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.PAUSE_BILLING);
+ assertEquals(events.get(4).getSubscriptionEventType(), SubscriptionEventType.RESUME_BILLING);
+
+ 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().getName(), "phase");
+ }
+
+
+
+ @Test(groups = "fast")
+ public void testOneEntitlementWithInitialBlockingState() 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 BlockingState bs1 = new DefaultBlockingState(UUID.randomUUID(), entitlementId, BlockingStateType.SUBSCRIPTION,
+ DefaultEntitlementApi.ENT_STATE_BLOCKED, DefaultEntitlementService.ENTITLEMENT_SERVICE_NAME,
+ true, true, false, clock.getUTCNow(), clock.getUTCNow(), clock.getUTCNow());
+
+ blockingStates.add(bs1);
+
+ clock.addDays(1);
+
+ 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 BlockingState bs2 = new DefaultBlockingState(UUID.randomUUID(), entitlementId, BlockingStateType.SUBSCRIPTION,
+ "NothingUseful", "boo",
+ false, false, false, clock.getUTCNow(), clock.getUTCNow(), clock.getUTCNow());
+
+ blockingStates.add(bs2);
+
+
+ effectiveDate = effectiveDate.plusDays(15);
+ clock.addDays(15);
+ final SubscriptionBaseTransition tr3 = createTransition(entitlementId, EventType.API_USER, ApiEventType.CANCEL, requestedDate, 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 DefaultSubscriptionBundleTimeline timeline = new DefaultSubscriptionBundleTimeline(accountTimeZone, accountId, bundleId, externalKey, entitlements, blockingStates);
+
+ assertEquals(timeline.getAccountId(), accountId);
+ assertEquals(timeline.getBundleId(), bundleId);
+ assertEquals(timeline.getExternalKey(), externalKey);
+
+ List<SubscriptionEvent> events = timeline.getSubscriptionEvents();
+ assertEquals(events.size(), 5);
+
+ 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(bs2.getEffectiveDate(), accountTimeZone)), 0);
+ assertEquals(events.get(4).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.STOP_BILLING);
+
+ 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().getName(), "phase");
+ assertEquals(events.get(4).getNextPhase(), null);
+ }
+
+
+
@Test(groups = "fast")
public void testOneEntitlementWithBlockingStatesSubscription() throws CatalogApiException {
@@ -191,7 +344,7 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
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(), null);
+ assertEquals(events.get(3).getNextPhase().getName(), "phase");
assertEquals(events.get(4).getPrevPhase().getName(), "phase");
assertEquals(events.get(4).getNextPhase(), null);
assertEquals(events.get(5).getPrevPhase().getName(), "phase");
@@ -289,15 +442,29 @@ public class TestDefaultSubscriptionBundleTimeline extends EntitlementTestSuiteN
assertEquals(events.get(7).getSubscriptionEventType(), SubscriptionEventType.STOP_ENTITLEMENT);
assertEquals(events.get(8).getSubscriptionEventType(), SubscriptionEventType.STOP_BILLING);
- assertEquals(events.get(0).getNextPhase().getName(), "trial");
+ assertEquals(events.get(0).getPrevPhase(), null);
+ assertEquals(events.get(0).getNextPhase().getName(), "trial1");
+ assertEquals(events.get(1).getPrevPhase(), null);
assertEquals(events.get(1).getNextPhase().getName(), "trial1");
+ assertEquals(events.get(2).getPrevPhase(), null);
assertEquals(events.get(2).getNextPhase().getName(), "phase2");
+ assertEquals(events.get(3).getPrevPhase(), null);
assertEquals(events.get(3).getNextPhase().getName(), "phase2");
+
+ assertEquals(events.get(4).getPrevPhase().getName(), "trial1");
assertEquals(events.get(4).getNextPhase().getName(), "phase1");
+
+ assertEquals(events.get(5).getPrevPhase().getName(), "phase1");
assertEquals(events.get(5).getNextPhase().getName(), "phase1");
- assertEquals(events.get(6).getNextPhase().getName(), "phase2");
- assertEquals(events.get(7).getNextPhase().getName(), "phase1");
- assertEquals(events.get(8).getNextPhase().getName(), "phase1");
+
+ assertEquals(events.get(6).getPrevPhase().getName(), "phase2");
+ assertEquals(events.get(6).getNextPhase(), null);
+
+ assertEquals(events.get(7).getPrevPhase().getName(), "phase1");
+ assertEquals(events.get(7).getNextPhase(), null);
+
+ assertEquals(events.get(8).getPrevPhase().getName(), "phase1");
+ assertEquals(events.get(8).getNextPhase(), null);
}