diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EventsStream.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EventsStream.java
index 9d07620..785e644 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EventsStream.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/EventsStream.java
@@ -123,6 +123,10 @@ public class EventsStream {
return entitlementCancelEvent != null && entitlementCancelEvent.getEffectiveDate().isAfter(utcNow);
}
+ public boolean isEntitlementFutureChanged() {
+ return getPendingSubscriptionEvents(utcNow, SubscriptionBaseTransitionType.CHANGE).iterator().hasNext();
+ }
+
public boolean isEntitlementActive() {
return entitlementState == EntitlementState.ACTIVE;
}
@@ -190,14 +194,7 @@ public class EventsStream {
}
public Collection<BlockingState> computeAddonsBlockingStatesForNextSubscriptionBaseEvent(final DateTime effectiveDate) {
- // Compute the transition trigger
- final Iterable<SubscriptionBaseTransition> pendingSubscriptionBaseTransitions = getPendingSubscriptionEvents(effectiveDate, SubscriptionBaseTransitionType.CHANGE, SubscriptionBaseTransitionType.CANCEL);
- if (!pendingSubscriptionBaseTransitions.iterator().hasNext()) {
- return ImmutableList.<BlockingState>of();
- }
-
- final SubscriptionBaseTransition subscriptionBaseTransitionTrigger = pendingSubscriptionBaseTransitions.iterator().next();
- return computeAddonsBlockingStatesForSubscriptionBaseEvent(subscriptionBaseTransitionTrigger);
+ return computeAddonsBlockingStatesForNextSubscriptionBaseEvent(effectiveDate, false);
}
// Compute future blocking states not on disk for add-ons associated to this (base) events stream
@@ -213,15 +210,31 @@ public class EventsStream {
// Note that in theory we could always only look subscription base as we assume entitlement cancel means subscription base cancel
// but we want to use the effective date of the entitlement cancel event to create the add-on cancel event
final BlockingState futureEntitlementCancelEvent = getEntitlementCancellationEvent(subscription.getId());
- return computeAddonsBlockingStatesForNextSubscriptionBaseEvent(futureEntitlementCancelEvent.getEffectiveDate());
- } else {
+ return computeAddonsBlockingStatesForNextSubscriptionBaseEvent(futureEntitlementCancelEvent.getEffectiveDate(), false);
+ } else if (isEntitlementFutureChanged()) {
// ...or a subscription change (i.e. a change plan where the new plan has an impact on the existing add-on).
// We need to go back to subscription base as entitlement doesn't know about these
- return computeAddonsBlockingStatesForNextSubscriptionBaseEvent(utcNow);
+ return computeAddonsBlockingStatesForNextSubscriptionBaseEvent(utcNow, true);
+ } else {
+ return ImmutableList.of();
+ }
+ }
+
+ private Collection<BlockingState> computeAddonsBlockingStatesForNextSubscriptionBaseEvent(final DateTime effectiveDate,
+ final boolean useBillingEffectiveDate) {
+ // Compute the transition trigger
+ final Iterable<SubscriptionBaseTransition> pendingSubscriptionBaseTransitions = getPendingSubscriptionEvents(effectiveDate, SubscriptionBaseTransitionType.CHANGE, SubscriptionBaseTransitionType.CANCEL);
+ if (!pendingSubscriptionBaseTransitions.iterator().hasNext()) {
+ return ImmutableList.<BlockingState>of();
}
+
+ final SubscriptionBaseTransition subscriptionBaseTransitionTrigger = pendingSubscriptionBaseTransitions.iterator().next();
+ return computeAddonsBlockingStatesForSubscriptionBaseEvent(subscriptionBaseTransitionTrigger,
+ useBillingEffectiveDate ? subscriptionBaseTransitionTrigger.getEffectiveTransitionTime() : effectiveDate);
}
- private Collection<BlockingState> computeAddonsBlockingStatesForSubscriptionBaseEvent(final SubscriptionBaseTransition subscriptionBaseTransitionTrigger) {
+ private Collection<BlockingState> computeAddonsBlockingStatesForSubscriptionBaseEvent(final SubscriptionBaseTransition subscriptionBaseTransitionTrigger,
+ final DateTime blockingStateEffectiveDate) {
if (baseSubscription == null || baseSubscription.getLastActivePlan() == null || !ProductCategory.BASE.equals(baseSubscription.getLastActivePlan().getProduct().getCategory())) {
return ImmutableList.<BlockingState>of();
}
@@ -284,7 +297,7 @@ public class EventsStream {
true,
true,
false,
- subscriptionBaseTransitionTrigger.getEffectiveTransitionTime());
+ blockingStateEffectiveDate);
}
});
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/engine/core/TestEntitlementUtils.java b/entitlement/src/test/java/com/ning/billing/entitlement/engine/core/TestEntitlementUtils.java
index abef851..498448f 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/engine/core/TestEntitlementUtils.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/engine/core/TestEntitlementUtils.java
@@ -158,6 +158,41 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
checkBlockingStatesDAO(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDate, true);
}
+ // See https://github.com/killbill/killbill/issues/121
+ @Test(groups = "slow", description = "Verify add-ons blocking states are not impacted by EOT billing cancellations")
+ public void testCancellationIMMBillingEOT() throws Exception {
+ // Approximate check, as the blocking state check (checkBlockingStatesDAO) could be a bit off
+ final DateTime cancellationDateTime = clock.getUTCNow();
+ final LocalDate cancellationDate = clock.getUTCToday();
+
+ // Cancel the base plan
+ testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.BLOCK);
+ final DefaultEntitlement cancelledBaseEntitlement = (DefaultEntitlement) baseEntitlement.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.END_OF_TERM, callContext);
+ assertListenerStatus();
+
+ // Refresh the add-on state
+ final DefaultEntitlement cancelledAddOnEntitlement = (DefaultEntitlement) entitlementApi.getEntitlementForId(addOnEntitlement.getId(), callContext);
+
+ // Verify we compute the right blocking states for the "read" path...
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledAddOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, null);
+ // ...and for the "write" path (which has been exercised in the cancel call above).
+ checkActualBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDateTime, true);
+ // Verify also the blocking states DAO doesn't add too many events (all on disk)
+ checkBlockingStatesDAO(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDate, true);
+
+ testListener.pushExpectedEvents(NextEvent.CANCEL, NextEvent.CANCEL);
+ clock.addDays(30);
+ assertListenerStatus();
+
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledAddOnEntitlement, null, null);
+ checkFutureBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, null);
+ checkActualBlockingStatesToCancel(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDateTime, true);
+ checkBlockingStatesDAO(cancelledBaseEntitlement, cancelledAddOnEntitlement, cancellationDate, true);
+ }
+
@Test(groups = "slow", description = "Verify add-ons blocking states are added for EOT change plans")
public void testChangePlanEOT() throws Exception {
// Change plan EOT to Assault-Rifle (Telescopic-Scope is included)