killbill-memoizeit

entitlement: override add-on end date if needed If a base

11/20/2013 8:14:16 AM

Details

diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscription.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscription.java
index 8f49b8a..25842b7 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscription.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/DefaultSubscription.java
@@ -37,7 +37,16 @@ public class DefaultSubscription extends DefaultEntitlement implements Subscript
 
     @Override
     public LocalDate getBillingEndDate() {
-        final DateTime futureOrCurrentEndDate = getSubscriptionBase().getEndDate() != null ? getSubscriptionBase().getEndDate() : getSubscriptionBase().getFutureEndDate();
+        final DateTime futureOrCurrentEndDateForSubscription = getSubscriptionBase().getEndDate() != null ? getSubscriptionBase().getEndDate() : getSubscriptionBase().getFutureEndDate();
+        final DateTime futureOrCurrentEndDateForBaseSubscription = getEventsStream().getBaseSubscription().getEndDate() != null ? getEventsStream().getBaseSubscription().getEndDate() : getEventsStream().getBaseSubscription().getFutureEndDate();
+
+        final DateTime futureOrCurrentEndDate;
+        if (futureOrCurrentEndDateForBaseSubscription != null && futureOrCurrentEndDateForBaseSubscription.isBefore(futureOrCurrentEndDateForSubscription)) {
+            futureOrCurrentEndDate = futureOrCurrentEndDateForBaseSubscription;
+        } else {
+            futureOrCurrentEndDate = futureOrCurrentEndDateForSubscription;
+        }
+
         return futureOrCurrentEndDate != null ? new LocalDate(futureOrCurrentEndDate, getAccount().getTimeZone()) : null;
     }
 
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/dao/ProxyBlockingStateDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/dao/ProxyBlockingStateDao.java
index bb9431c..a154d28 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/dao/ProxyBlockingStateDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/dao/ProxyBlockingStateDao.java
@@ -215,6 +215,9 @@ public class ProxyBlockingStateDao implements BlockingStateDao {
             throw new RuntimeException(e);
         }
 
+        // Retrieve the cancellation blocking state on disk, if it exists (will be used later)
+        final BlockingState cancellationBlockingStateOnDisk = findEntitlementCancellationBlockingState(blockableId, blockingStatesOnDiskCopy);
+
         // Compute the blocking states not on disk for all base subscriptions
         final DateTime now = clock.getUTCNow();
         for (final SubscriptionBase baseSubscription : baseSubscriptionsToConsider) {
@@ -226,18 +229,31 @@ public class ProxyBlockingStateDao implements BlockingStateDao {
                 throw new RuntimeException(e);
             }
 
+            // First, check to see if the base entitlement is cancelled. If so, cancel the
             final Collection<BlockingState> blockingStatesNotOnDisk = eventsStream.computeAddonsBlockingStatesForFutureSubscriptionBaseEvents();
 
             // Inject the extra blocking states into the stream if needed
             for (final BlockingState blockingState : blockingStatesNotOnDisk) {
-                if ((blockingStateType == null ||
-                     // In case we're coming from getBlockingHistoryForService / getBlockingAll, make sure we don't add
-                     // blocking states for other add-ons on that base subscription
-                     (BlockingStateType.SUBSCRIPTION.equals(blockingStateType) && blockingState.getBlockedId().equals(blockableId))) &&
-                    // If this entitlement is actually already cancelled, don't add the cancellation event we computed from subscription events (wrong)
-                    !isEntitlementCancelled(blockingState.getBlockedId(), blockingStatesOnDiskCopy)) {
+                // If this entitlement is actually already cancelled, add the cancellation event we computed
+                // only if it's prior to the blocking state on disk (e.g. add-on future cancelled but base plan cancelled earlier).
+                final boolean overrideCancellationBlockingStateOnDisk = cancellationBlockingStateOnDisk != null &&
+                                                                        isEntitlementCancellationBlockingState(blockingState) &&
+                                                                        blockingState.getEffectiveDate().isBefore(cancellationBlockingStateOnDisk.getEffectiveDate());
+
+                if ((
+                            blockingStateType == null ||
+                            // In case we're coming from getBlockingHistoryForService / getBlockingAll, make sure we don't add
+                            // blocking states for other add-ons on that base subscription
+                            (BlockingStateType.SUBSCRIPTION.equals(blockingStateType) && blockingState.getBlockedId().equals(blockableId))
+                    ) && (
+                            cancellationBlockingStateOnDisk == null || overrideCancellationBlockingStateOnDisk
+                    )) {
                     final BlockingStateModelDao blockingStateModelDao = new BlockingStateModelDao(blockingState, now, now);
                     blockingStatesOnDiskCopy.add(BlockingStateModelDao.toBlockingState(blockingStateModelDao));
+
+                    if (overrideCancellationBlockingStateOnDisk) {
+                        blockingStatesOnDiskCopy.remove(cancellationBlockingStateOnDisk);
+                    }
                 }
             }
         }
@@ -246,18 +262,25 @@ public class ProxyBlockingStateDao implements BlockingStateDao {
         return BLOCKING_STATE_ORDERING.immutableSortedCopy(blockingStatesOnDiskCopy);
     }
 
-    private boolean isEntitlementCancelled(final UUID blockedId, final Iterable<BlockingState> blockingStates) {
-        // If this entitlement is already cancelled, there is nothing to do
-        return Iterables.<BlockingState>tryFind(blockingStates,
+    private BlockingState findEntitlementCancellationBlockingState(@Nullable final UUID blockedId, final Iterable<BlockingState> blockingStatesOnDisk) {
+        if (blockedId == null) {
+            return null;
+        }
+
+        return Iterables.<BlockingState>tryFind(blockingStatesOnDisk,
                                                 new Predicate<BlockingState>() {
                                                     @Override
                                                     public boolean apply(final BlockingState input) {
                                                         return input.getBlockedId().equals(blockedId) &&
-                                                               BlockingStateType.SUBSCRIPTION.equals(input.getType()) &&
-                                                               EntitlementService.ENTITLEMENT_SERVICE_NAME.equals(input.getService()) &&
-                                                               DefaultEntitlementApi.ENT_STATE_CANCELLED.equals(input.getStateName());
+                                                               isEntitlementCancellationBlockingState(input);
                                                     }
                                                 })
-                        .orNull() != null;
+                        .orNull();
+    }
+
+    private static boolean isEntitlementCancellationBlockingState(final BlockingState blockingState) {
+        return BlockingStateType.SUBSCRIPTION.equals(blockingState.getType()) &&
+               EntitlementService.ENTITLEMENT_SERVICE_NAME.equals(blockingState.getService()) &&
+               DefaultEntitlementApi.ENT_STATE_CANCELLED.equals(blockingState.getStateName());
     }
 }
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 6f40045..97650e3 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
@@ -97,6 +97,10 @@ public class EventsStream {
         return bundle;
     }
 
+    public SubscriptionBase getBaseSubscription() {
+        return baseSubscription;
+    }
+
     public SubscriptionBase getSubscription() {
         return subscription;
     }
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 2bc92f5..a0c387c 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
@@ -318,9 +318,6 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
         final Entitlement addOn2Entitlement = entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOn2Spec, initialDate, callContext);
         assertListenerStatus();
 
-        final LocalDate baseCancellationDate = new LocalDate(2013, 10, 10);
-        baseEntitlement.cancelEntitlementWithDate(baseCancellationDate, true, callContext);
-
         // Date prior to the base cancellation date to verify it is not impacted by the base cancellation (in contrary to the second add-on)
         final LocalDate addOn1CancellationDate = new LocalDate(2013, 9, 9);
         addOnEntitlement.cancelEntitlementWithDate(addOn1CancellationDate, true, callContext);
@@ -328,13 +325,22 @@ public class TestEntitlementUtils extends EntitlementTestSuiteWithEmbeddedDB {
         final LocalDate addOn2CancellationDate = new LocalDate(2013, 11, 11);
         addOn2Entitlement.cancelEntitlementWithDate(addOn2CancellationDate, true, callContext);
 
+        // Before the base entitlement is cancelled, respect the specified cancellation date
+        Assert.assertEquals(entitlementApi.getEntitlementForId(addOn2Entitlement.getId(), callContext).getEffectiveEndDate(), addOn2CancellationDate);
+
+        final LocalDate baseCancellationDate = new LocalDate(2013, 10, 10);
+        baseEntitlement.cancelEntitlementWithDate(baseCancellationDate, true, callContext);
+
+        // After the base entitlement is cancelled, verify the date is overridden
+        Assert.assertEquals(entitlementApi.getEntitlementForId(addOn2Entitlement.getId(), callContext).getEffectiveEndDate(), baseCancellationDate);
+
         // No further event yet
         assertListenerStatus();
 
         // Verify the cancellation dates
         Assert.assertEquals(entitlementApi.getEntitlementForId(baseEntitlement.getId(), callContext).getEffectiveEndDate(), baseCancellationDate);
         Assert.assertEquals(entitlementApi.getEntitlementForId(addOnEntitlement.getId(), callContext).getEffectiveEndDate(), addOn1CancellationDate);
-        Assert.assertEquals(entitlementApi.getEntitlementForId(addOn2Entitlement.getId(), callContext).getEffectiveEndDate(), addOn2CancellationDate);
+        Assert.assertEquals(entitlementApi.getEntitlementForId(addOn2Entitlement.getId(), callContext).getEffectiveEndDate(), baseCancellationDate);
 
         testListener.pushExpectedEvents(NextEvent.CANCEL, NextEvent.CANCEL, NextEvent.CANCEL, NextEvent.BLOCK, NextEvent.BLOCK, NextEvent.BLOCK, NextEvent.BLOCK);
         clock.setDay(new LocalDate(2013, 10, 30));