killbill-aplcache

Changes

Details

diff --git a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
index c413f0d..7153f48 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
@@ -180,7 +180,8 @@ public class TestAnalyticsService
             Subscription.SubscriptionState.ACTIVE,
             plan,
             phase,
-            priceList
+            priceList,
+            true
         );
         expectedTransition = new BusinessSubscriptionTransition(
             ID,
diff --git a/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java b/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
index f592c41..6bc4d7a 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
@@ -19,6 +19,7 @@ package com.ning.billing.analytics;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionTransition;
@@ -146,11 +147,16 @@ public class MockSubscription implements Subscription
 
 	@Override
 	public DateTime getPaidThroughDate() {
-		throw new UnsupportedOperationException();
+        throw new UnsupportedOperationException();
 	}
 
     @Override
     public SubscriptionTransition getPreviousTransition() {
         return null;
     }
+
+    @Override
+    public ProductCategory getCategory() {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java b/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
index a9a8293..0c3cf62 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
@@ -175,7 +175,8 @@ public class TestAnalyticsListener
             nextState,
             plan,
             phase,
-            priceList
+            priceList,
+            true
         );
     }
 
@@ -212,7 +213,8 @@ public class TestAnalyticsListener
             null,
             null,
             null,
-            null
+            null,
+            true
         );
     }
 
@@ -239,7 +241,8 @@ public class TestAnalyticsListener
             nextState,
             plan,
             phase,
-            priceList
+            priceList,
+            true
         );
     }
 }
\ No newline at end of file
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java b/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
index 5c626fc..0765f30 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
@@ -19,6 +19,8 @@ package com.ning.billing.entitlement.api.user;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
+
 import org.joda.time.DateTime;
 
 import java.util.List;
@@ -69,6 +71,7 @@ public interface Subscription {
 
     public DateTime getPaidThroughDate();
 
+    public ProductCategory getCategory();
 
     public List<SubscriptionTransition> getActiveTransitions();
 
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 363e948..137fb52 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -83,6 +83,14 @@
             <artifactId>management-dbfiles</artifactId>
             <scope>test</scope>
         </dependency>
+        <!-- Same here, this is really debatable whether or not we should keep that here -->
+        <dependency>
+            <groupId>com.ning.billing</groupId>
+            <artifactId>killbill-account</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java
index 26248e7..0b372e8 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java
@@ -189,7 +189,8 @@ public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
                 .setActiveVersion(subscriptionData.getActiveVersion())
                 .setEffectiveDate(cur.getEventTime())
                 .setProcessedDate(now)
-                .setRequestedDate(now);
+                .setRequestedDate(now)
+                .setFromDisk(true);
 
                 switch(cur.getApiEventType()) {
                 case MIGRATE_ENTITLEMENT:
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
index d265697..5bd8596 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
@@ -168,12 +168,13 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
             throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_AO_BP_NON_ACTIVE, targetAddOnPlan.getName());
         }
 
-        if (addonUtils.isAddonIncluded(baseSubscription, targetAddOnPlan)) {
+        Product baseProduct = baseSubscription.getCurrentPlan().getProduct();
+        if (addonUtils.isAddonIncluded(baseProduct, targetAddOnPlan)) {
             throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_AO_ALREADY_INCLUDED,
                     targetAddOnPlan.getName(), baseSubscription.getCurrentPlan().getProduct().getName());
         }
 
-        if (!addonUtils.isAddonAvailable(baseSubscription, targetAddOnPlan)) {
+        if (!addonUtils.isAddonAvailable(baseProduct, targetAddOnPlan)) {
             throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_AO_NOT_AVAILABLE,
                     targetAddOnPlan.getName(), baseSubscription.getCurrentPlan().getProduct().getName());
         }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
index 1c523df..9a82a99 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
@@ -69,7 +69,8 @@ public class SubscriptionApiService {
             .setActiveVersion(subscription.getActiveVersion())
             .setProcessedDate(processedDate)
             .setEffectiveDate(effectiveDate)
-            .setRequestedDate(requestedDate));
+            .setRequestedDate(requestedDate)
+            .setFromDisk(true));
 
             TimedPhase nextTimedPhase = curAndNextPhases[1];
             PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
@@ -117,7 +118,8 @@ public class SubscriptionApiService {
             .setActiveVersion(subscription.getActiveVersion())
             .setProcessedDate(now)
             .setEffectiveDate(effectiveDate)
-            .setRequestedDate(now));
+            .setRequestedDate(now)
+            .setFromDisk(true));
 
             dao.cancelSubscription(subscription.getId(), cancelEvent);
             subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId()), catalogService.getFullCatalog());
@@ -140,7 +142,8 @@ public class SubscriptionApiService {
         .setActiveVersion(subscription.getActiveVersion())
         .setProcessedDate(now)
         .setRequestedDate(now)
-        .setEffectiveDate(now));
+        .setEffectiveDate(now)
+        .setFromDisk(true));
 
         List<EntitlementEvent> uncancelEvents = new ArrayList<EntitlementEvent>();
         uncancelEvents.add(uncancelEvent);
@@ -212,7 +215,8 @@ public class SubscriptionApiService {
             .setActiveVersion(subscription.getActiveVersion())
             .setProcessedDate(now)
             .setEffectiveDate(effectiveDate)
-            .setRequestedDate(now));
+            .setRequestedDate(now)
+            .setFromDisk(true));
 
             TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), requestedDate, effectiveDate);
             PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
index d94af98..ce341fd 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
@@ -209,9 +209,11 @@ public class SubscriptionData implements Subscription {
         }
 
         // ensure that the latestSubscription is always set; prevents NPEs
-        SubscriptionTransition latestSubscription = transitions.get(0);
-        for (SubscriptionTransition cur : transitions) {
-            if (cur.getEffectiveTransitionTime().isAfter(clock.getUTCNow())) {
+        SubscriptionTransitionData latestSubscription = transitions.get(0);
+        for (SubscriptionTransitionData cur : transitions) {
+            if (cur.getEffectiveTransitionTime().isAfter(clock.getUTCNow()) ||
+                    // We are not looking at events that were patched on the fly-- such as future ADDON cancelation from Base Plan
+                   !cur.isFromDisk()) {
                 break;
             }
             latestSubscription = cur;
@@ -236,6 +238,7 @@ public class SubscriptionData implements Subscription {
         return activeVersion;
     }
 
+    @Override
     public ProductCategory getCategory() {
         return category;
     }
@@ -359,6 +362,8 @@ public class SubscriptionData implements Subscription {
 
             ApiEventType apiEventType = null;
 
+            boolean isFromDisk = true;
+
             switch (cur.getType()) {
 
             case PHASE:
@@ -369,6 +374,7 @@ public class SubscriptionData implements Subscription {
             case API_USER:
                 ApiEvent userEV = (ApiEvent) cur;
                 apiEventType = userEV.getEventType();
+                isFromDisk = userEV.isFromDisk();
                 switch(apiEventType) {
                 case MIGRATE_ENTITLEMENT:
                 case CREATE:
@@ -430,7 +436,8 @@ public class SubscriptionData implements Subscription {
                         nextState,
                         nextPlan,
                         nextPhase,
-                        nextPriceList);
+                        nextPriceList,
+                        isFromDisk);
             transitions.add(transition);
 
             previousState = nextState;
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
index 5a0eba0..fb8e2f2 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
@@ -44,11 +44,12 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
     private final String nextPriceList;
     private final Plan nextPlan;
     private final PlanPhase nextPhase;
+    private final boolean isFromDisk;
 
     public SubscriptionTransitionData(UUID eventId, UUID subscriptionId, UUID bundleId, EventType eventType,
             ApiEventType apiEventType, DateTime requestedTransitionTime, DateTime effectiveTransitionTime,
             SubscriptionState previousState, Plan previousPlan, PlanPhase previousPhase, String previousPriceList,
-            SubscriptionState nextState, Plan nextPlan, PlanPhase nextPhase, String nextPriceList) {
+            SubscriptionState nextState, Plan nextPlan, PlanPhase nextPhase, String nextPriceList, boolean isFromDisk) {
         super();
         this.eventId = eventId;
         this.subscriptionId = subscriptionId;
@@ -65,6 +66,7 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
         this.nextPlan = nextPlan;
         this.nextPriceList = nextPriceList;
         this.nextPhase = nextPhase;
+        this.isFromDisk = isFromDisk;
     }
 
     @Override
@@ -136,14 +138,6 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
         }
     }
 
-    public ApiEventType getApiEventType() {
-        return apiEventType;
-    }
-
-    public EventType getEventType() {
-        return eventType;
-    }
-
     @Override
     public DateTime getRequestedTransitionTime() {
         return requestedTransitionTime;
@@ -154,6 +148,19 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
         return effectiveTransitionTime;
     }
 
+    public boolean isFromDisk() {
+        return isFromDisk;
+    }
+
+    public ApiEventType getApiEventType() {
+        return apiEventType;
+    }
+
+    public EventType getEventType() {
+        return eventType;
+    }
+
+
 
     @Override
     public String toString() {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/addon/AddonUtils.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/addon/AddonUtils.java
index 94c35f9..b2c9405 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/addon/AddonUtils.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/addon/AddonUtils.java
@@ -16,6 +16,10 @@
 
 package com.ning.billing.entitlement.engine.addon;
 
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.google.inject.Inject;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.catalog.api.CatalogApiException;
@@ -26,9 +30,11 @@ import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
 import com.ning.billing.entitlement.api.user.SubscriptionData;
 import com.ning.billing.entitlement.api.user.SubscriptionTransition;
+import com.ning.billing.entitlement.exceptions.EntitlementError;
 
 public class AddonUtils {
 
+    private static final Logger logger = LoggerFactory.getLogger(AddonUtils.class);
 
     private final CatalogService catalogService;
 
@@ -37,14 +43,19 @@ public class AddonUtils {
         this.catalogService = catalogService;
     }
 
-    public boolean isAddonAvailable(SubscriptionData baseSubscription, Plan targetAddOnPlan) {
 
-        if (baseSubscription.getState() == SubscriptionState.CANCELLED) {
-            return false;
+    public boolean isAddonAvailable(final String basePlanName, final DateTime requestedDate, final Plan targetAddOnPlan) {
+        try {
+            Plan plan = catalogService.getFullCatalog().findPlan(basePlanName, requestedDate);
+            Product product = plan.getProduct();
+            return isAddonAvailable(product, targetAddOnPlan);
+        } catch (CatalogApiException e) {
+            throw new EntitlementError(e);
         }
+    }
 
+    public boolean isAddonAvailable(final Product baseProduct, final Plan targetAddOnPlan) {
         Product targetAddonProduct = targetAddOnPlan.getProduct();
-        Product baseProduct = baseSubscription.getCurrentPlan().getProduct();
         Product[] availableAddOns = baseProduct.getAvailable();
 
         for (Product curAv : availableAddOns) {
@@ -55,15 +66,18 @@ public class AddonUtils {
         return false;
     }
 
-    public boolean isAddonIncluded(SubscriptionData baseSubscription, Plan targetAddOnPlan) {
-
-        if (baseSubscription.getState() == SubscriptionState.CANCELLED) {
-            return false;
+    public boolean isAddonIncluded(final String basePlanName,  final DateTime requestedDate, final Plan targetAddOnPlan) {
+        try {
+            Plan plan = catalogService.getFullCatalog().findPlan(basePlanName, requestedDate);
+            Product product = plan.getProduct();
+            return isAddonIncluded(product, targetAddOnPlan);
+        } catch (CatalogApiException e) {
+            throw new EntitlementError(e);
         }
+    }
 
+    public boolean isAddonIncluded(final Product baseProduct, final Plan targetAddOnPlan) {
         Product targetAddonProduct = targetAddOnPlan.getProduct();
-        Product baseProduct = baseSubscription.getCurrentPlan().getProduct();
-
         Product[] includedAddOns = baseProduct.getIncluded();
         for (Product curAv : includedAddOns) {
             if (curAv.getName().equals(targetAddonProduct.getName())) {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
index 427353a..55ea2cf 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
@@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
 import com.google.inject.Inject;
 
 import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.Product;
 import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.config.EntitlementConfig;
 import com.ning.billing.entitlement.alignment.PlanAligner;
@@ -225,7 +226,11 @@ public class Engine implements EventListener, EntitlementService {
 
         DateTime now = clock.getUTCNow();
 
+        Product baseProduct = (baseSubscription.getState() == SubscriptionState.CANCELLED ) ?
+                null : baseSubscription.getCurrentPlan().getProduct();
+
         List<Subscription> subscriptions = dao.getSubscriptions(baseSubscription.getBundleId());
+
         Iterator<Subscription> it = subscriptions.iterator();
         while (it.hasNext()) {
             SubscriptionData cur = (SubscriptionData) it.next();
@@ -234,9 +239,9 @@ public class Engine implements EventListener, EntitlementService {
                 continue;
             }
             Plan addonCurrentPlan = cur.getCurrentPlan();
-            // If base Plan has been canceled, that will cancel all the OA
-            if (addonUtils.isAddonIncluded(baseSubscription, addonCurrentPlan) ||
-                    ! addonUtils.isAddonAvailable(baseSubscription, addonCurrentPlan)) {
+            if (baseProduct == null ||
+                    addonUtils.isAddonIncluded(baseProduct, addonCurrentPlan) ||
+                    ! addonUtils.isAddonAvailable(baseProduct, addonCurrentPlan)) {
                 //
                 // Perform AO cancellation using the effectiveDate of the BP
                 //
@@ -245,7 +250,8 @@ public class Engine implements EventListener, EntitlementService {
                 .setActiveVersion(cur.getActiveVersion())
                 .setProcessedDate(now)
                 .setEffectiveDate(event.getEffectiveDate())
-                .setRequestedDate(now));
+                .setRequestedDate(now)
+                .setFromDisk(true));
                 dao.cancelSubscription(cur.getId(), cancelEvent);
             }
         }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
index 9507061..ef3168f 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
@@ -17,7 +17,9 @@
 package com.ning.billing.entitlement.engine.dao;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.UUID;
@@ -30,8 +32,12 @@ import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
 import com.google.inject.Inject;
 import com.ning.billing.ErrorCode;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.Product;
 import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.migration.AccountMigrationData;
 import com.ning.billing.entitlement.api.migration.AccountMigrationData.BundleMigrationData;
@@ -42,10 +48,14 @@ import com.ning.billing.entitlement.api.user.SubscriptionBundleData;
 import com.ning.billing.entitlement.api.user.SubscriptionData;
 import com.ning.billing.entitlement.api.user.SubscriptionFactory;
 import com.ning.billing.entitlement.api.user.SubscriptionFactory.SubscriptionBuilder;
+import com.ning.billing.entitlement.engine.addon.AddonUtils;
 import com.ning.billing.entitlement.engine.core.Engine;
 import com.ning.billing.entitlement.events.EntitlementEvent;
 import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
 import com.ning.billing.entitlement.events.user.ApiEvent;
+import com.ning.billing.entitlement.events.user.ApiEventBuilder;
+import com.ning.billing.entitlement.events.user.ApiEventCancel;
+import com.ning.billing.entitlement.events.user.ApiEventChange;
 import com.ning.billing.entitlement.events.user.ApiEventType;
 import com.ning.billing.entitlement.exceptions.EntitlementError;
 import com.ning.billing.util.clock.Clock;
@@ -65,16 +75,18 @@ public class EntitlementSqlDao implements EntitlementDao {
     private final EventSqlDao eventsDao;
     private final SubscriptionFactory factory;
     private final NotificationQueueService notificationQueueService;
+    private final AddonUtils addonUtils;
 
     @Inject
     public EntitlementSqlDao(final IDBI dbi, final Clock clock, final SubscriptionFactory factory,
-                             final NotificationQueueService notificationQueueService) {
+            final AddonUtils addonUtils, final NotificationQueueService notificationQueueService) {
         this.clock = clock;
         this.factory = factory;
         this.subscriptionsDao = dbi.onDemand(SubscriptionSqlDao.class);
         this.eventsDao = dbi.onDemand(EventSqlDao.class);
         this.bundlesDao = dbi.onDemand(BundleSqlDao.class);
         this.notificationQueueService = notificationQueueService;
+        this.addonUtils = addonUtils;
     }
 
     @Override
@@ -104,10 +116,6 @@ public class EntitlementSqlDao implements EntitlementDao {
         });
     }
 
-    @Override
-    public Subscription getSubscriptionFromId(final UUID subscriptionId) {
-        return buildSubscription(subscriptionsDao.getSubscriptionFromId(subscriptionId.toString()));
-    }
 
     @Override
     public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId) {
@@ -134,19 +142,18 @@ public class EntitlementSqlDao implements EntitlementDao {
 
     @Override
     public Subscription getBaseSubscription(final UUID bundleId) {
+        return getBaseSubscription(bundleId, true);
+    }
 
-        List<Subscription> subscriptions = subscriptionsDao.getSubscriptionsFromBundleId(bundleId.toString());
-        for (Subscription cur : subscriptions) {
-            if (((SubscriptionData)cur).getCategory() == ProductCategory.BASE) {
-                return  buildSubscription(cur);
-            }
-        }
-        return null;
+
+    @Override
+    public Subscription getSubscriptionFromId(final UUID subscriptionId) {
+        return buildSubscription(subscriptionsDao.getSubscriptionFromId(subscriptionId.toString()));
     }
 
     @Override
     public List<Subscription> getSubscriptions(UUID bundleId) {
-        return buildSubscription(subscriptionsDao.getSubscriptionsFromBundleId(bundleId.toString()));
+        return buildBundleSubscriptions(subscriptionsDao.getSubscriptionsFromBundleId(bundleId.toString()));
     }
 
     @Override
@@ -155,7 +162,7 @@ public class EntitlementSqlDao implements EntitlementDao {
         if (bundle == null) {
             return Collections.emptyList();
         }
-        return buildSubscription(subscriptionsDao.getSubscriptionsFromBundleId(bundle.getId().toString()));
+        return getSubscriptions(bundle.getId());
     }
 
     @Override
@@ -359,18 +366,106 @@ public class EntitlementSqlDao implements EntitlementDao {
         }
     }
 
+    public Subscription getBaseSubscription(final UUID bundleId, boolean rebuildSubscription) {
+        List<Subscription> subscriptions = subscriptionsDao.getSubscriptionsFromBundleId(bundleId.toString());
+        for (Subscription cur : subscriptions) {
+            if (((SubscriptionData)cur).getCategory() == ProductCategory.BASE) {
+                return  rebuildSubscription ? buildSubscription(cur) : cur;
+            }
+        }
+        return null;
+    }
+
+
     private Subscription buildSubscription(Subscription input) {
         if (input == null) {
             return null;
         }
-        return buildSubscription(Collections.singletonList(input)).get(0);
+        List<Subscription> bundleInput = new ArrayList<Subscription>();
+        Subscription baseSubscription = null;
+        if (input.getCategory() == ProductCategory.ADD_ON) {
+            baseSubscription = getBaseSubscription(input.getBundleId(), false);
+            bundleInput.add(baseSubscription);
+            bundleInput.add(input);
+        } else {
+            bundleInput.add(input);
+        }
+        List<Subscription> reloadedSubscriptions = buildBundleSubscriptions(bundleInput);
+        for (Subscription cur : reloadedSubscriptions) {
+            if (cur.getId().equals(input.getId())) {
+                return cur;
+            }
+         }
+         throw new EntitlementError(String.format("Unexpected code path in buildSubscription"));
     }
 
-    private List<Subscription> buildSubscription(List<Subscription> input) {
-        List<Subscription> result = new ArrayList<Subscription>(input.size());
+
+
+    private List<Subscription> buildBundleSubscriptions(List<Subscription> input) {
+
+        // Make sure BasePlan -- if exists-- is first
+        Collections.sort(input, new Comparator<Subscription>() {
+            @Override
+            public int compare(Subscription o1, Subscription o2) {
+                if (o1.getCategory() == ProductCategory.BASE) {
+                    return -1;
+                } else if (o2.getCategory() == ProductCategory.BASE) {
+                    return 1;
+                } else {
+                    return o1.getStartDate().compareTo(o2.getStartDate());
+                }
+            }
+        });
+
+        EntitlementEvent futureBaseEvent = null;
+                List<Subscription> result = new ArrayList<Subscription>(input.size());
         for (Subscription cur : input) {
+
             List<EntitlementEvent> events = eventsDao.getEventsForSubscription(cur.getId().toString());
-            Subscription reloaded =   factory.createSubscription(new SubscriptionBuilder((SubscriptionData) cur), events);
+            Subscription reloaded = factory.createSubscription(new SubscriptionBuilder((SubscriptionData) cur), events);
+
+            switch (cur.getCategory()) {
+            case BASE:
+                Collection<EntitlementEvent> futureApiEvents = Collections2.filter(events, new Predicate<EntitlementEvent>() {
+                    @Override
+                    public boolean apply(EntitlementEvent input) {
+                        return (input.getEffectiveDate().isAfter(clock.getUTCNow()) &&
+                                ((input instanceof ApiEventCancel) || (input instanceof ApiEventChange)));
+                    }
+                });
+                futureBaseEvent = (futureApiEvents.size() == 0) ? null : futureApiEvents.iterator().next();
+                break;
+
+            case ADD_ON:
+                Plan targetAddOnPlan = reloaded.getCurrentPlan();
+                String baseProductName = (futureBaseEvent instanceof ApiEventChange) ?
+                        ((ApiEventChange) futureBaseEvent).getEventPlan() : null;
+
+                boolean createCancelEvent = (futureBaseEvent != null) &&
+                    ((futureBaseEvent instanceof ApiEventCancel) ||
+                            ((! addonUtils.isAddonAvailable(baseProductName, futureBaseEvent.getEffectiveDate(), targetAddOnPlan)) ||
+                                    (addonUtils.isAddonIncluded(baseProductName, futureBaseEvent.getEffectiveDate(), targetAddOnPlan))));
+
+                if (createCancelEvent) {
+                    DateTime now = clock.getUTCNow();
+                    EntitlementEvent addOnCancelEvent = new ApiEventCancel(new ApiEventBuilder()
+                    .setSubscriptionId(reloaded.getId())
+                    .setActiveVersion(((SubscriptionData) reloaded).getActiveVersion())
+                    .setProcessedDate(now)
+                    .setEffectiveDate(futureBaseEvent.getEffectiveDate())
+                    .setRequestedDate(now)
+                    // This event is only there to indicate the ADD_ON is future canceled, but it is not there
+                    // on disk until the base plan cancellation becomes effective
+                    .setFromDisk(false));
+
+                    events.add(addOnCancelEvent);
+                    // Finally reload subscription with full set of events
+                    reloaded = factory.createSubscription(new SubscriptionBuilder((SubscriptionData) cur), events);
+                }
+                break;
+            default:
+                break;
+            }
             result.add(reloaded);
         }
         return result;
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
index 5f485e5..4e2128e 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
@@ -139,7 +139,8 @@ public interface EventSqlDao extends Transactional<EventSqlDao>, CloseMe, Transm
                     .setEventPlan(planName)
                     .setEventPlanPhase(phaseName)
                     .setEventPriceList(priceListName)
-                    .setEventType(userType);
+                    .setEventType(userType)
+                    .setFromDisk(true);
 
                 if (userType == ApiEventType.CREATE) {
                     result = new ApiEventCreate(builder);
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEvent.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEvent.java
index ecd5aa1..c26b168 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEvent.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEvent.java
@@ -29,4 +29,6 @@ public interface ApiEvent extends EntitlementEvent {
 
     public String getPriceList();
 
+    public boolean isFromDisk();
+
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBase.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBase.java
index 2438ddf..f67c6b7 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBase.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBase.java
@@ -28,7 +28,7 @@ public class ApiEventBase extends EventBase implements ApiEvent {
     private final String eventPlan;
     private final String eventPlanPhase;
     private final String eventPriceList;
-
+    private final boolean fromDisk;
 
     public ApiEventBase(ApiEventBuilder builder) {
         super(builder);
@@ -36,9 +36,10 @@ public class ApiEventBase extends EventBase implements ApiEvent {
         this.eventPriceList = builder.getEventPriceList();
         this.eventPlan = builder.getEventPlan();
         this.eventPlanPhase = builder.getEventPlanPhase();
+        this.fromDisk = builder.isFromDisk();
     }
 
-
+/*
     public ApiEventBase(UUID subscriptionId, DateTime bundleStartDate, DateTime processed, String planName, String phaseName,
             String priceList, DateTime requestedDate,  ApiEventType eventType, DateTime effectiveDate, long activeVersion) {
         super(subscriptionId, requestedDate, effectiveDate, processed, activeVersion, true);
@@ -56,7 +57,7 @@ public class ApiEventBase extends EventBase implements ApiEvent {
         this.eventPlan = null;
         this.eventPlanPhase = null;
     }
-
+*/
 
     @Override
     public ApiEventType getEventType() {
@@ -83,6 +84,11 @@ public class ApiEventBase extends EventBase implements ApiEvent {
         return eventPriceList;
     }
 
+    @Override
+    public boolean isFromDisk() {
+        return fromDisk;
+    }
+
 
     @Override
     public String toString() {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
index 2ff026b..b7e9764 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
@@ -24,6 +24,8 @@ public class ApiEventBuilder extends EventBaseBuilder<ApiEventBuilder> {
     private String eventPlan;
     private String eventPlanPhase;
     private String eventPriceList;
+    private boolean fromDisk;
+
 
     public ApiEventBuilder() {
         super();
@@ -49,6 +51,15 @@ public class ApiEventBuilder extends EventBaseBuilder<ApiEventBuilder> {
         return eventPriceList;
     }
 
+    public boolean isFromDisk() {
+        return fromDisk;
+    }
+
+    public ApiEventBuilder setFromDisk(boolean fromDisk) {
+        this.fromDisk = fromDisk;
+        return this;
+    }
+
     public ApiEventBuilder setEventType(ApiEventType eventType) {
         this.eventType = eventType;
         return this;
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java
index 98cc376..fab66dd 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java
@@ -24,6 +24,7 @@ import org.joda.time.DateTime;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionTransition;
@@ -131,14 +132,13 @@ public class BrainDeadSubscription implements Subscription {
 
 	@Override
 	public List<SubscriptionTransition> getAllTransitions() {
-		throw new UnsupportedOperationException();
+        throw new UnsupportedOperationException();
 
 	}
 
 	@Override
 	public SubscriptionTransition getPendingTransition() {
 		throw new UnsupportedOperationException();
-
 	}
 
     @Override
@@ -146,4 +146,9 @@ public class BrainDeadSubscription implements Subscription {
         return null;
     }
 
+    @Override
+    public ProductCategory getCategory() {
+        throw new UnsupportedOperationException();
+    }
+
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java
index c62fa07..55d383a 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java
@@ -174,7 +174,7 @@ public class TestDefaultEntitlementBillingApi {
 		PlanPhase nextPhase = nextPlan.getAllPhases()[0]; // The trial has no billing period
 		String nextPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
 		SubscriptionTransition t = new SubscriptionTransitionData(
-				zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList);
+				zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList, true);
 		transitions.add(t);
 
 		AccountUserApi accountApi = new BrainDeadAccountUserApi(){
@@ -199,7 +199,7 @@ public class TestDefaultEntitlementBillingApi {
 		PlanPhase nextPhase = nextPlan.getAllPhases()[1];
 		String nextPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
 		SubscriptionTransition t = new SubscriptionTransitionData(
-				zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList);
+				zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList, true);
 		transitions.add(t);
 
 		Account account = BrainDeadProxyFactory.createBrainDeadProxyFor(Account.class);
@@ -221,7 +221,7 @@ public class TestDefaultEntitlementBillingApi {
 		PlanPhase nextPhase = nextPlan.getAllPhases()[1];
 		String nextPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
 		SubscriptionTransition t = new SubscriptionTransitionData(
-				zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList);
+				zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList, true);
 		transitions.add(t);
 
 		AccountUserApi accountApi = new BrainDeadAccountUserApi(){
@@ -246,7 +246,7 @@ public class TestDefaultEntitlementBillingApi {
 		PlanPhase nextPhase = nextPlan.getAllPhases()[0];
 		String nextPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
 		SubscriptionTransition t = new SubscriptionTransitionData(
-				zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList);
+				zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList, true);
 		transitions.add(t);
 
 		Account account = BrainDeadProxyFactory.createBrainDeadProxyFor(Account.class);
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java
index a5b0142..7db938e 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java
@@ -123,6 +123,8 @@ public class TestUserApiAddOn extends TestApiBase {
             // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
             aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
             assertEquals(aoSubscription.getState(), SubscriptionState.ACTIVE);
+            assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+
 
             // MOVE AFTER CANCELLATION
             testListener.reset();
@@ -168,7 +170,7 @@ public class TestUserApiAddOn extends TestApiBase {
             clock.setDeltaFromReality(twoMonths, DAY_IN_MS);
             assertTrue(testListener.isCompleted(5000));
 
-            // SET CTD TO CANCEL IN FUTURE
+            // SET CTD TO CHANGE IN FUTURE
             DateTime now = clock.getUTCNow();
             Duration ctd = getDurationMonth(1);
             DateTime newChargedThroughDate = DefaultClock.addDuration(now, ctd);
@@ -239,13 +241,13 @@ public class TestUserApiAddOn extends TestApiBase {
             // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
             aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
             assertEquals(aoSubscription.getState(), SubscriptionState.ACTIVE);
+            assertTrue(aoSubscription.isSubscriptionFutureCancelled());
 
             // MOVE AFTER CHANGE
             testListener.reset();
             testListener.pushExpectedEvent(NextEvent.CHANGE);
             testListener.pushExpectedEvent(NextEvent.CANCEL);
             clock.addDeltaFromReality(ctd);
-            now = clock.getUTCNow();
             assertTrue(testListener.isCompleted(5000));
 
 
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java
index c5881f9..cb87dc0 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java
@@ -25,6 +25,7 @@ import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
 
 import com.google.inject.Inject;
 import com.ning.billing.entitlement.api.user.SubscriptionFactory;
+import com.ning.billing.entitlement.engine.addon.AddonUtils;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.notificationq.NotificationQueueService;
 
@@ -33,8 +34,8 @@ public class MockEntitlementDaoSql extends EntitlementSqlDao implements MockEnti
     private final ResetSqlDao resetDao;
 
     @Inject
-    public MockEntitlementDaoSql(IDBI dbi, Clock clock, SubscriptionFactory factory, NotificationQueueService notificationQueueService) {
-        super(dbi, clock, factory, notificationQueueService);
+    public MockEntitlementDaoSql(IDBI dbi, Clock clock, SubscriptionFactory factory, AddonUtils addonUtils, NotificationQueueService notificationQueueService) {
+        super(dbi, clock, factory, addonUtils, notificationQueueService);
         this.resetDao = dbi.onDemand(ResetSqlDao.class);
     }
 
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/MockSubscription.java b/invoice/src/test/java/com/ning/billing/invoice/dao/MockSubscription.java
index 7a9253c..c0b054a 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/MockSubscription.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/MockSubscription.java
@@ -19,6 +19,7 @@ package com.ning.billing.invoice.dao;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionTransition;
@@ -124,4 +125,9 @@ public class MockSubscription implements Subscription {
     public SubscriptionTransition getPreviousTransition() {
         return null;
     }
+
+    @Override
+    public ProductCategory getCategory() {
+        throw new UnsupportedOperationException();
+    }
 }
\ No newline at end of file
diff --git a/invoice/src/test/java/com/ning/billing/invoice/notification/BrainDeadSubscription.java b/invoice/src/test/java/com/ning/billing/invoice/notification/BrainDeadSubscription.java
index 587144b..05d7c79 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/notification/BrainDeadSubscription.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/notification/BrainDeadSubscription.java
@@ -24,6 +24,7 @@ import org.joda.time.DateTime;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionTransition;
@@ -47,103 +48,108 @@ public class BrainDeadSubscription implements Subscription {
 			throws EntitlementUserApiException {
 		throw new UnsupportedOperationException();
 
-		
+
 	}
 
 	@Override
 	public void pause() throws EntitlementUserApiException {
 		throw new UnsupportedOperationException();
 
-		
+
 	}
 
 	@Override
 	public void resume() throws EntitlementUserApiException {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public UUID getId() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public UUID getBundleId() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public SubscriptionState getState() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public DateTime getStartDate() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public DateTime getEndDate() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public Plan getCurrentPlan() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public String getCurrentPriceList() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public PlanPhase getCurrentPhase() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public DateTime getChargedThroughDate() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public DateTime getPaidThroughDate() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public List<SubscriptionTransition> getActiveTransitions() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public List<SubscriptionTransition> getAllTransitions() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public SubscriptionTransition getPendingTransition() {
 		throw new UnsupportedOperationException();
-		
+
 	}
 
 	@Override
 	public SubscriptionTransition getPreviousTransition() {
-		throw new UnsupportedOperationException();
+        throw new UnsupportedOperationException();
 	}
 
+    @Override
+    public ProductCategory getCategory() {
+        throw new UnsupportedOperationException();
+    }
+
 }