killbill-memoizeit

analytics: revisit bst listener On requested or effective

7/2/2012 10:36:51 PM

Details

diff --git a/analytics/pom.xml b/analytics/pom.xml
index ecd27bf..c95818d 100644
--- a/analytics/pom.xml
+++ b/analytics/pom.xml
@@ -117,7 +117,6 @@
         <dependency>
             <groupId>com.ning.billing</groupId>
             <artifactId>killbill-util</artifactId>
-            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>com.ning.billing</groupId>
diff --git a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
index 247a438..bcddd64 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
@@ -25,7 +25,6 @@ import com.ning.billing.entitlement.api.timeline.RepairEntitlementEvent;
 import com.ning.billing.entitlement.api.user.EffectiveSubscriptionEvent;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.RequestedSubscriptionEvent;
-import com.ning.billing.entitlement.api.user.SubscriptionEvent;
 import com.ning.billing.invoice.api.EmptyInvoiceEvent;
 import com.ning.billing.invoice.api.InvoiceCreationEvent;
 import com.ning.billing.payment.api.PaymentErrorEvent;
@@ -58,14 +57,18 @@ public class AnalyticsListener {
 
     @Subscribe
     public void handleEffectiveSubscriptionTransitionChange(final EffectiveSubscriptionEvent eventEffective) throws AccountApiException, EntitlementUserApiException {
-        handleSubscriptionTransitionChange(eventEffective);
+        bstRecorder.rebuildTransitionsForBundle(eventEffective.getBundleId());
     }
 
     @Subscribe
     public void handleRequestedSubscriptionTransitionChange(final RequestedSubscriptionEvent eventRequested) throws AccountApiException, EntitlementUserApiException {
-        if (eventRequested.getEffectiveTransitionTime().isAfter(eventRequested.getRequestedTransitionTime())) {
-            handleSubscriptionTransitionChange(eventRequested);
-        }
+        bstRecorder.rebuildTransitionsForBundle(eventRequested.getBundleId());
+    }
+
+    @Subscribe
+    public void handleRepairEntitlement(final RepairEntitlementEvent event) {
+        // In case of repair, just rebuild all transitions
+        bstRecorder.rebuildTransitionsForBundle(event.getBundleId());
     }
 
     @Subscribe
@@ -84,6 +87,7 @@ public class AnalyticsListener {
 
     @Subscribe
     public void handleInvoiceCreation(final InvoiceCreationEvent event) {
+        // TODO - follow same logic as entitlements to support repair
         invoiceRecorder.invoiceCreated(event.getInvoiceId());
     }
 
@@ -141,37 +145,4 @@ public class AnalyticsListener {
     public void handleUserTagDefinitionDeletion(final UserTagDefinitionDeletionEvent event) {
         // Ignored for now
     }
-
-    @Subscribe
-    public void handleRepairEntitlement(final RepairEntitlementEvent event) {
-        // Ignored for now
-    }
-
-    private void handleSubscriptionTransitionChange(final SubscriptionEvent eventEffective) throws AccountApiException, EntitlementUserApiException {
-        switch (eventEffective.getTransitionType()) {
-            // A subscription enters either through migration or as newly created subscription
-            case MIGRATE_ENTITLEMENT:
-            case CREATE:
-                bstRecorder.subscriptionCreated(eventEffective);
-                break;
-            case RE_CREATE:
-                bstRecorder.subscriptionRecreated(eventEffective);
-                break;
-            case MIGRATE_BILLING:
-                break;
-            case CANCEL:
-                bstRecorder.subscriptionCancelled(eventEffective);
-                break;
-            case CHANGE:
-                bstRecorder.subscriptionChanged(eventEffective);
-                break;
-            case UNCANCEL:
-                break;
-            case PHASE:
-                bstRecorder.subscriptionPhaseChanged(eventEffective);
-                break;
-            default:
-                throw new RuntimeException("Unexpected event type " + eventEffective.getTransitionType());
-        }
-    }
 }
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
index e0fbab0..a4c9ff3 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
@@ -16,7 +16,9 @@
 
 package com.ning.billing.analytics;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.skife.jdbi.v2.Transaction;
@@ -34,10 +36,14 @@ import com.ning.billing.analytics.model.BusinessSubscriptionEvent;
 import com.ning.billing.analytics.model.BusinessSubscriptionTransition;
 import com.ning.billing.catalog.api.CatalogService;
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.entitlement.api.SubscriptionTransitionType;
+import com.ning.billing.entitlement.api.user.EffectiveSubscriptionEvent;
 import com.ning.billing.entitlement.api.user.EntitlementUserApi;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
+import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.entitlement.api.user.SubscriptionEvent;
+import com.ning.billing.util.clock.Clock;
 
 public class BusinessSubscriptionTransitionRecorder {
     private static final Logger log = LoggerFactory.getLogger(BusinessSubscriptionTransitionRecorder.class);
@@ -46,90 +52,194 @@ public class BusinessSubscriptionTransitionRecorder {
     private final EntitlementUserApi entitlementApi;
     private final AccountUserApi accountApi;
     private final CatalogService catalogService;
+    private final Clock clock;
 
     @Inject
-    public BusinessSubscriptionTransitionRecorder(final BusinessSubscriptionTransitionSqlDao sqlDao, final CatalogService catalogService, final EntitlementUserApi entitlementApi, final AccountUserApi accountApi) {
+    public BusinessSubscriptionTransitionRecorder(final BusinessSubscriptionTransitionSqlDao sqlDao,
+                                                  final CatalogService catalogService,
+                                                  final EntitlementUserApi entitlementApi,
+                                                  final AccountUserApi accountApi,
+                                                  final Clock clock) {
         this.sqlDao = sqlDao;
         this.catalogService = catalogService;
         this.entitlementApi = entitlementApi;
         this.accountApi = accountApi;
+        this.clock = clock;
     }
 
-    public void subscriptionCreated(final SubscriptionEvent created) throws AccountApiException, EntitlementUserApiException {
-        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionCreated(created.getNextPlan(), catalogService.getFullCatalog(), created.getEffectiveTransitionTime(), created.getSubscriptionStartDate());
-        recordTransition(event, created);
+    public void rebuildTransitionsForBundle(final UUID bundleId) {
+        final SubscriptionBundle bundle;
+        try {
+            bundle = entitlementApi.getBundleFromId(bundleId);
+        } catch (EntitlementUserApiException e) {
+            log.warn("Ignoring update for bundle {}: bundle does not exist", bundleId);
+            return;
+        }
+
+        final Account account;
+        try {
+            account = accountApi.getAccountById(bundle.getAccountId());
+        } catch (AccountApiException e) {
+            log.warn("Ignoring update for bundle {}: account {} does not exist", bundleId, bundle.getAccountId());
+            return;
+        }
+
+        final List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundleId);
+
+        final String externalKey = bundle.getKey();
+        final String accountKey = account.getExternalKey();
+        final Currency currency = account.getCurrency();
+
+        sqlDao.inTransaction(new Transaction<Void, BusinessSubscriptionTransitionSqlDao>() {
+            @Override
+            public Void inTransaction(final BusinessSubscriptionTransitionSqlDao transactional, final TransactionStatus status) throws Exception {
+                log.info("Started rebuilding transitions for bundle {}", externalKey);
+                transactional.deleteTransitionsForBundle(externalKey);
+
+                final ArrayList<BusinessSubscriptionTransition> transitions = new ArrayList<BusinessSubscriptionTransition>();
+                for (final Subscription subscription : subscriptions) {
+                    for (final EffectiveSubscriptionEvent event : subscription.getAllTransitions()) {
+                        final BusinessSubscriptionEvent businessEvent = getBusinessSubscriptionFromEvent(event);
+                        if (businessEvent == null) {
+                            continue;
+                        }
+
+                        final BusinessSubscription prevSubscription = createPreviousBusinessSubscription(event, businessEvent, transitions, currency);
+                        final BusinessSubscription nextSubscription = createNextBusinessSubscription(event, businessEvent, currency);
+                        final BusinessSubscriptionTransition transition = new BusinessSubscriptionTransition(
+                                event.getTotalOrdering(),
+                                externalKey,
+                                accountKey,
+                                event.getRequestedTransitionTime(),
+                                businessEvent,
+                                prevSubscription,
+                                nextSubscription
+                        );
+
+                        transactional.createTransition(transition);
+                        transitions.add(transition);
+                        log.info("Adding transition {}", transition);
+
+                        // We need to manually add the system cancel event
+                        if (SubscriptionTransitionType.CANCEL.equals(event.getTransitionType()) &&
+                                clock.getUTCNow().isAfter(event.getEffectiveTransitionTime())) {
+                            final BusinessSubscriptionTransition systemCancelTransition = new BusinessSubscriptionTransition(
+                                    event.getTotalOrdering(),
+                                    externalKey,
+                                    accountKey,
+                                    event.getRequestedTransitionTime(),
+                                    new BusinessSubscriptionEvent(BusinessSubscriptionEvent.EventType.SYSTEM_CANCEL, businessEvent.getCategory()),
+                                    prevSubscription,
+                                    nextSubscription
+                            );
+                            transactional.createTransition(systemCancelTransition);
+                            transitions.add(systemCancelTransition);
+                            log.info("Adding transition {}", systemCancelTransition);
+                        }
+                    }
+                }
+
+                log.info("Finished rebuilding transitions for bundle {}", externalKey);
+                return null;
+            }
+        });
+    }
+
+    private BusinessSubscriptionEvent getBusinessSubscriptionFromEvent(final SubscriptionEvent event) throws AccountApiException, EntitlementUserApiException {
+        switch (event.getTransitionType()) {
+            // A subscription enters either through migration or as newly created subscription
+            case MIGRATE_ENTITLEMENT:
+            case CREATE:
+                return subscriptionCreated(event);
+            case RE_CREATE:
+                return subscriptionRecreated(event);
+            case CANCEL:
+                return subscriptionCancelled(event);
+            case CHANGE:
+                return subscriptionChanged(event);
+            case PHASE:
+                return subscriptionPhaseChanged(event);
+            // TODO - should we really ignore these?
+            case MIGRATE_BILLING:
+            case UNCANCEL:
+                return null;
+            default:
+                log.warn("Unexpected event type " + event.getTransitionType());
+                return null;
+        }
+    }
+
+    private BusinessSubscriptionEvent subscriptionCreated(final SubscriptionEvent created) throws AccountApiException, EntitlementUserApiException {
+        return BusinessSubscriptionEvent.subscriptionCreated(created.getNextPlan(), catalogService.getFullCatalog(), created.getEffectiveTransitionTime(), created.getSubscriptionStartDate());
     }
 
-    public void subscriptionRecreated(final SubscriptionEvent recreated) throws AccountApiException, EntitlementUserApiException {
-        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionRecreated(recreated.getNextPlan(), catalogService.getFullCatalog(), recreated.getEffectiveTransitionTime(), recreated.getSubscriptionStartDate());
-        recordTransition(event, recreated);
+    private BusinessSubscriptionEvent subscriptionRecreated(final SubscriptionEvent recreated) throws AccountApiException, EntitlementUserApiException {
+        return BusinessSubscriptionEvent.subscriptionRecreated(recreated.getNextPlan(), catalogService.getFullCatalog(), recreated.getEffectiveTransitionTime(), recreated.getSubscriptionStartDate());
     }
 
-    public void subscriptionCancelled(final SubscriptionEvent cancelled) throws AccountApiException, EntitlementUserApiException {
+    private BusinessSubscriptionEvent subscriptionCancelled(final SubscriptionEvent cancelled) throws AccountApiException, EntitlementUserApiException {
         // cancelled.getNextPlan() is null here - need to look at the previous one to create the correct event name
-        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionCancelled(cancelled.getPreviousPlan(), catalogService.getFullCatalog(), cancelled.getEffectiveTransitionTime(), cancelled.getSubscriptionStartDate());
-        recordTransition(event, cancelled);
+        return BusinessSubscriptionEvent.subscriptionCancelled(cancelled.getPreviousPlan(), catalogService.getFullCatalog(), cancelled.getEffectiveTransitionTime(), cancelled.getSubscriptionStartDate());
     }
 
-    public void subscriptionChanged(final SubscriptionEvent changed) throws AccountApiException, EntitlementUserApiException {
-        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionChanged(changed.getNextPlan(), catalogService.getFullCatalog(), changed.getEffectiveTransitionTime(), changed.getSubscriptionStartDate());
-        recordTransition(event, changed);
+    private BusinessSubscriptionEvent subscriptionChanged(final SubscriptionEvent changed) throws AccountApiException, EntitlementUserApiException {
+        return BusinessSubscriptionEvent.subscriptionChanged(changed.getNextPlan(), catalogService.getFullCatalog(), changed.getEffectiveTransitionTime(), changed.getSubscriptionStartDate());
     }
 
-    public void subscriptionPhaseChanged(final SubscriptionEvent phaseChanged) throws AccountApiException, EntitlementUserApiException {
-        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionPhaseChanged(phaseChanged.getNextPlan(), phaseChanged.getNextState(), catalogService.getFullCatalog(), phaseChanged.getEffectiveTransitionTime(), phaseChanged.getSubscriptionStartDate());
-        recordTransition(event, phaseChanged);
+    private BusinessSubscriptionEvent subscriptionPhaseChanged(final SubscriptionEvent phaseChanged) throws AccountApiException, EntitlementUserApiException {
+        return BusinessSubscriptionEvent.subscriptionPhaseChanged(phaseChanged.getNextPlan(), phaseChanged.getNextState(), catalogService.getFullCatalog(), phaseChanged.getEffectiveTransitionTime(), phaseChanged.getSubscriptionStartDate());
     }
 
-    void recordTransition(final BusinessSubscriptionEvent event, final SubscriptionEvent transition) throws AccountApiException, EntitlementUserApiException {
-        Currency currency = null;
-        String externalKey = null;
-        String accountKey = null;
+    private BusinessSubscription createNextBusinessSubscription(final EffectiveSubscriptionEvent event, final BusinessSubscriptionEvent businessEvent, final Currency currency) {
+        final BusinessSubscription nextSubscription;
+        if (BusinessSubscriptionEvent.EventType.CANCEL.equals(businessEvent.getEventType()) ||
+                BusinessSubscriptionEvent.EventType.SYSTEM_CANCEL.equals(businessEvent.getEventType())) {
+            nextSubscription = null;
+        } else {
+            nextSubscription = new BusinessSubscription(event.getNextPriceList(), event.getNextPlan(), event.getNextPhase(),
+                                                        currency, event.getEffectiveTransitionTime(), event.getNextState(),
+                                                        event.getSubscriptionId(), event.getBundleId(), catalogService.getFullCatalog());
+        }
 
-        // Retrieve key and currency via the bundle
-        final SubscriptionBundle bundle = entitlementApi.getBundleFromId(transition.getBundleId());
-        if (bundle != null) {
-            externalKey = bundle.getKey();
+        return nextSubscription;
+    }
 
-            final Account account = accountApi.getAccountById(bundle.getAccountId());
-            if (account != null) {
-                accountKey = account.getExternalKey();
-                currency = account.getCurrency();
-            }
+    private BusinessSubscription createPreviousBusinessSubscription(final EffectiveSubscriptionEvent event,
+                                                                    final BusinessSubscriptionEvent businessEvent,
+                                                                    final ArrayList<BusinessSubscriptionTransition> transitions,
+                                                                    final Currency currency) {
+        if (BusinessSubscriptionEvent.EventType.ADD.equals(businessEvent.getEventType()) ||
+                BusinessSubscriptionEvent.EventType.RE_ADD.equals(businessEvent.getEventType())) {
+            return null;
         }
 
-        // The SubscriptionEvent interface gives us all the prev/next information we need but the start date
-        // of the previous phase. We need to retrieve it from our own transitions table
-        DateTime previousEffectiveTransitionTime = null;
-        // For (re-)creation events, the prev subscription will always be null
-        if (!BusinessSubscriptionEvent.EventType.ADD.equals(event.getEventType()) && !BusinessSubscriptionEvent.EventType.RE_ADD.equals(event.getEventType())) {
-            final List<BusinessSubscriptionTransition> transitions = sqlDao.getTransitions(externalKey);
-            if (transitions != null && transitions.size() > 0) {
-                for (final BusinessSubscriptionTransition candidate : transitions) {
-                    if (candidate != null && candidate.getNextSubscription() != null && candidate.getNextSubscription().getStartDate().isBefore(transition.getEffectiveTransitionTime())) {
-                        previousEffectiveTransitionTime = candidate.getNextSubscription().getStartDate();
-                    }
-                }
+        final BusinessSubscriptionTransition prevTransition = getPreviousBusinessSubscriptionTransitionForEvent(event, transitions);
+        return new BusinessSubscription(event.getPreviousPriceList(), event.getPreviousPlan(), event.getPreviousPhase(),
+                                        currency, prevTransition.getNextSubscription().getStartDate(), event.getPreviousState(),
+                                        event.getSubscriptionId(), event.getBundleId(), catalogService.getFullCatalog());
+    }
+
+    private BusinessSubscriptionTransition getPreviousBusinessSubscriptionTransitionForEvent(final EffectiveSubscriptionEvent event,
+                                                                                             final ArrayList<BusinessSubscriptionTransition> transitions) {
+        BusinessSubscriptionTransition transition = null;
+        for (final BusinessSubscriptionTransition candidate : transitions) {
+            final BusinessSubscription nextSubscription = candidate.getNextSubscription();
+            if (nextSubscription == null || !nextSubscription.getStartDate().isBefore(event.getEffectiveTransitionTime())) {
+                continue;
             }
-        }
 
-        // TODO Support currency changes
-        final BusinessSubscription prevSubscription;
-        if (previousEffectiveTransitionTime == null) {
-            prevSubscription = null;
-        } else {
-            prevSubscription = new BusinessSubscription(transition.getPreviousPriceList(), transition.getPreviousPlan(), transition.getPreviousPhase(), currency, previousEffectiveTransitionTime, transition.getPreviousState(), transition.getSubscriptionId(), transition.getBundleId(), catalogService.getFullCatalog());
+            if (nextSubscription.getSubscriptionId().equals(event.getSubscriptionId())) {
+                transition = candidate;
+            }
         }
-        final BusinessSubscription nextSubscription;
 
-        // next plan is null for CANCEL events
-        if (transition.getNextPlan() == null) {
-            nextSubscription = null;
-        } else {
-            nextSubscription = new BusinessSubscription(transition.getNextPriceList(), transition.getNextPlan(), transition.getNextPhase(), currency, transition.getEffectiveTransitionTime(), transition.getNextState(), transition.getSubscriptionId(), transition.getBundleId(), catalogService.getFullCatalog());
+        if (transition == null) {
+            log.error("Unable to retrieve the previous transition - THIS SHOULD NEVER HAPPEN");
+            // Fall back to the latest one?
+            transition = transitions.get(transitions.size() - 1);
         }
 
-        record(transition.getTotalOrdering(), externalKey, accountKey, transition.getRequestedTransitionTime(), event, prevSubscription, nextSubscription);
+        return transition;
     }
 
     // Public for internal reasons
@@ -143,59 +253,6 @@ public class BusinessSubscriptionTransitionRecorder {
                 prevSubscription,
                 nextSubscription
         );
-
-        log.info(transition.getEvent() + " " + transition);
-        sqlDao.inTransaction(new Transaction<Void, BusinessSubscriptionTransitionSqlDao>() {
-            @Override
-            public Void inTransaction(final BusinessSubscriptionTransitionSqlDao transactional, final TransactionStatus status) throws Exception {
-                final String subscriptionId;
-                if (nextSubscription != null && nextSubscription.getSubscriptionId() != null) {
-                    subscriptionId = nextSubscription.getSubscriptionId().toString();
-                } else {
-                    subscriptionId = prevSubscription.getSubscriptionId().toString();
-                }
-
-                // There is no ordering guaranteed with events on the bus. This can be problematic on e.g. subscription creation:
-                // the requested future change from trial to evergreen could be received before the actual creation event.
-                // In this case, we would have two subscriptions in BST, with both null for the previous transition.
-                // To work around this, we need to update bst as we go
-                if (BusinessSubscriptionEvent.EventType.ADD.equals(event.getEventType())) {
-                    final List<BusinessSubscriptionTransition> transitions = transactional.getTransitionForSubscription(subscriptionId);
-                    if (transitions != null && transitions.size() > 0) {
-                        final BusinessSubscriptionTransition firstTransition = transitions.get(0);
-                        // Ignore (re-)creation events here, the previous subscription is expected to be null
-                        if (!BusinessSubscriptionEvent.EventType.ADD.equals(firstTransition.getEvent().getEventType()) &&
-                                !BusinessSubscriptionEvent.EventType.RE_ADD.equals(firstTransition.getEvent().getEventType()) &&
-                                firstTransition.getPreviousSubscription() == null) {
-                            final BusinessSubscriptionTransition updatedFirstTransition = new BusinessSubscriptionTransition(
-                                    firstTransition.getTotalOrdering(),
-                                    firstTransition.getExternalKey(),
-                                    firstTransition.getAccountKey(),
-                                    firstTransition.getRequestedTimestamp(),
-                                    firstTransition.getEvent(),
-                                    nextSubscription,
-                                    firstTransition.getNextSubscription()
-                            );
-                            transactional.updateTransition(updatedFirstTransition.getTotalOrdering(), updatedFirstTransition);
-                        }
-                    }
-                }
-
-                // Ignore duplicates: for e.g. phase events, we may already have recorded the transition when the change
-                // was requested. In that case, ignore it
-                final List<BusinessSubscriptionTransition> currentTransitions = transactional.getTransitionForSubscription(subscriptionId);
-                if (currentTransitions != null && currentTransitions.size() > 0) {
-                    for (final BusinessSubscriptionTransition currentTransition : currentTransitions) {
-                        if (currentTransition.isDuplicateOf(transition)) {
-                            return null;
-                        }
-                    }
-                }
-
-                transactional.createTransition(transition);
-
-                return null;
-            }
-        });
+        sqlDao.createTransition(transition);
     }
 }
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
index 5b2838c..2092d47 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
@@ -43,5 +43,8 @@ public interface BusinessSubscriptionTransitionSqlDao extends Transactional<Busi
     void updateTransition(@Bind("total_ordering") long totalOrdering, @BusinessSubscriptionTransitionBinder BusinessSubscriptionTransition updatedFirstTransition);
 
     @SqlUpdate
+    void deleteTransitionsForBundle(@Bind("external_key") final String externalKey);
+
+    @SqlUpdate
     void test();
 }
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
index 2959682..7192e5f 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
+++ b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
@@ -37,7 +37,7 @@ getTransitions(external_key) ::= <<
   , next_bundle_id
   from bst
   where external_key=:external_key
-  order by prev_start_date, next_start_date, requested_timestamp asc
+  order by requested_timestamp asc
   ;
 >>
 
@@ -192,6 +192,12 @@ updateTransition() ::= <<
   where total_ordering = :total_ordering
 >>
 
+deleteTransitionsForBundle(external_key) ::= <<
+  delete from bst
+  where external_key=:external_key
+  ;
+>>
+
 test() ::= <<
   select 1 from bst;
 >>
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql b/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
index 43aaad8..366570c 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
+++ b/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
@@ -1,6 +1,7 @@
 drop table if exists bst;
 create table bst (
-  total_ordering bigint default 0
+  record_id int(11) unsigned not null auto_increment
+, total_ordering bigint default 0
 , external_key varchar(50) not null comment 'Bundle external key'
 , account_key varchar(50) not null comment 'Account external key'
 , requested_timestamp bigint not null
@@ -33,7 +34,7 @@ create table bst (
 , next_state varchar(32) default null
 , next_subscription_id varchar(100) default null
 , next_bundle_id varchar(100) default null
-, primary key(total_ordering)
+, primary key(record_id)
 ) engine=innodb comment 'Business Subscription Transitions, track bundles lifecycle';
 create index bst_key_index on bst (external_key, requested_timestamp asc);
 
diff --git a/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java b/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
index 8083833..b54d7ce 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
@@ -58,6 +58,11 @@ public class MockBusinessSubscriptionTransitionSqlDao implements BusinessSubscri
     }
 
     @Override
+    public void deleteTransitionsForBundle(@Bind("external_key") final String externalKey) {
+        content.put(externalKey, new ArrayList<BusinessSubscriptionTransition>());
+    }
+
+    @Override
     public void test() {
     }
 
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransitionRecorder.java b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransitionRecorder.java
index 9dbc209..48cefb4 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransitionRecorder.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransitionRecorder.java
@@ -24,23 +24,24 @@ import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.ImmutableList;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountUserApi;
 import com.ning.billing.analytics.dao.BusinessSubscriptionTransitionSqlDao;
-import com.ning.billing.analytics.model.BusinessSubscription;
-import com.ning.billing.analytics.model.BusinessSubscriptionEvent;
 import com.ning.billing.analytics.model.BusinessSubscriptionTransition;
 import com.ning.billing.catalog.api.Catalog;
 import com.ning.billing.catalog.api.CatalogService;
-import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.entitlement.api.SubscriptionTransitionType;
 import com.ning.billing.entitlement.api.user.EffectiveSubscriptionEvent;
 import com.ning.billing.entitlement.api.user.EntitlementUserApi;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.util.clock.DefaultClock;
 
 public class TestBusinessSubscriptionTransitionRecorder extends AnalyticsTestSuite {
     @Test(groups = "fast")
     public void testCreateAddOn() throws Exception {
+        final UUID bundleId = UUID.randomUUID();
         final UUID externalKey = UUID.randomUUID();
 
         // Setup the catalog
@@ -49,26 +50,10 @@ public class TestBusinessSubscriptionTransitionRecorder extends AnalyticsTestSui
 
         // Setup the dao
         final BusinessSubscriptionTransitionSqlDao sqlDao = new MockBusinessSubscriptionTransitionSqlDao();
-        // Add a previous subscription to make sure it doesn't impact the addon
-        final BusinessSubscription nextPrevSubscription = new BusinessSubscription(UUID.randomUUID().toString(),
-                                                                                   UUID.randomUUID().toString(),
-                                                                                   UUID.randomUUID().toString(),
-                                                                                   Currency.USD,
-                                                                                   new DateTime(DateTimeZone.UTC),
-                                                                                   Subscription.SubscriptionState.ACTIVE,
-                                                                                   UUID.randomUUID(),
-                                                                                   UUID.randomUUID(),
-                                                                                   catalogService.getFullCatalog());
-        sqlDao.createTransition(new BusinessSubscriptionTransition(10L,
-                                                                externalKey.toString(),
-                                                                UUID.randomUUID().toString(),
-                                                                new DateTime(DateTimeZone.UTC),
-                                                                BusinessSubscriptionEvent.valueOf("ADD_MISC"),
-                                                                null,
-                                                                nextPrevSubscription));
 
         // Setup the entitlement API
         final SubscriptionBundle bundle = Mockito.mock(SubscriptionBundle.class);
+        Mockito.when(bundle.getId()).thenReturn(bundleId);
         Mockito.when(bundle.getKey()).thenReturn(externalKey.toString());
         final EntitlementUserApi entitlementApi = Mockito.mock(EntitlementUserApi.class);
         Mockito.when(entitlementApi.getBundleFromId(Mockito.<UUID>any())).thenReturn(bundle);
@@ -79,20 +64,25 @@ public class TestBusinessSubscriptionTransitionRecorder extends AnalyticsTestSui
         final AccountUserApi accountApi = Mockito.mock(AccountUserApi.class);
         Mockito.when(accountApi.getAccountById(bundle.getAccountId())).thenReturn(account);
 
-        final BusinessSubscriptionTransitionRecorder recorder = new BusinessSubscriptionTransitionRecorder(sqlDao, catalogService, entitlementApi, accountApi);
-
         // Create an new subscription event
         final EffectiveSubscriptionEvent eventEffective = Mockito.mock(EffectiveSubscriptionEvent.class);
         Mockito.when(eventEffective.getId()).thenReturn(UUID.randomUUID());
+        Mockito.when(eventEffective.getTransitionType()).thenReturn(SubscriptionTransitionType.CREATE);
         Mockito.when(eventEffective.getSubscriptionId()).thenReturn(UUID.randomUUID());
         Mockito.when(eventEffective.getRequestedTransitionTime()).thenReturn(new DateTime(DateTimeZone.UTC));
         Mockito.when(eventEffective.getNextPlan()).thenReturn(UUID.randomUUID().toString());
         Mockito.when(eventEffective.getEffectiveTransitionTime()).thenReturn(new DateTime(DateTimeZone.UTC));
         Mockito.when(eventEffective.getSubscriptionStartDate()).thenReturn(new DateTime(DateTimeZone.UTC));
-        recorder.subscriptionCreated(eventEffective);
 
-        Assert.assertEquals(sqlDao.getTransitions(externalKey.toString()).size(), 2);
-        final BusinessSubscriptionTransition transition = sqlDao.getTransitions(externalKey.toString()).get(1);
+        final Subscription subscription = Mockito.mock(Subscription.class);
+        Mockito.when(subscription.getAllTransitions()).thenReturn(ImmutableList.<EffectiveSubscriptionEvent>of(eventEffective));
+        Mockito.when(entitlementApi.getSubscriptionsForBundle(Mockito.<UUID>any())).thenReturn(ImmutableList.<Subscription>of(subscription));
+
+        final BusinessSubscriptionTransitionRecorder recorder = new BusinessSubscriptionTransitionRecorder(sqlDao, catalogService, entitlementApi, accountApi, new DefaultClock());
+        recorder.rebuildTransitionsForBundle(bundle.getId());
+
+        Assert.assertEquals(sqlDao.getTransitions(externalKey.toString()).size(), 1);
+        final BusinessSubscriptionTransition transition = sqlDao.getTransitions(externalKey.toString()).get(0);
         Assert.assertEquals(transition.getTotalOrdering(), (long) eventEffective.getTotalOrdering());
         Assert.assertEquals(transition.getAccountKey(), externalKey.toString());
         // Make sure all the prev_ columns are null
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java
index 6f18739..79e762f 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java
@@ -85,7 +85,7 @@ public class TestBusinessTagRecorder extends TestWithEmbeddedDB {
         final CatalogService catalogService = new DefaultCatalogService(Mockito.mock(CatalogConfig.class), Mockito.mock(VersionedCatalogLoader.class));
         final AddonUtils addonUtils = new AddonUtils(catalogService);
         final DefaultNotificationQueueService notificationQueueService = new DefaultNotificationQueueService(dbi, clock);
-        final EntitlementDao entitlementDao = new AuditedEntitlementDao(dbi, clock, addonUtils, notificationQueueService, eventBus);
+        final EntitlementDao entitlementDao = new AuditedEntitlementDao(dbi, clock, addonUtils, notificationQueueService, eventBus, catalogService);
         final PlanAligner planAligner = new PlanAligner(catalogService);
         final DefaultSubscriptionApiService apiService = new DefaultSubscriptionApiService(clock, entitlementDao, catalogService, planAligner);
         final DefaultSubscriptionFactory subscriptionFactory = new DefaultSubscriptionFactory(apiService, clock, catalogService);
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
index 8a72880..94421a4 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
@@ -98,10 +98,11 @@ public class TestAnalytics extends TestIntegrationBase {
         subscription.cancel(clock.getUTCNow(), true, context);
 
         waitALittle();
+
         verifyBSTWithTrialAndEvergreenPhasesAndCancellation(account, bundle, subscription);
 
         // Move after cancel date
-        clock.addDeltaFromReality(AT_LEAST_ONE_MONTH_MS);
+        clock.addDeltaFromReality(AT_LEAST_ONE_MONTH_MS + 100);
         assertTrue(busHandler.isCompleted(DELAY));
         waitALittle();
 
@@ -296,7 +297,7 @@ public class TestAnalytics extends TestIntegrationBase {
         Assert.assertEquals(transitions.size(), 3);
 
         verifyTrialAndEvergreenPhases(account, bundle, subscription);
-        verifyCancellationTransition(account, bundle, subscription);
+        verifyCancellationTransition(account, bundle);
     }
 
     private void verifyBSTWithTrialAndEvergreenPhasesAndCancellationAndSystemCancellation(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws CatalogApiException {
@@ -305,8 +306,8 @@ public class TestAnalytics extends TestIntegrationBase {
         Assert.assertEquals(transitions.size(), 4);
 
         verifyTrialAndEvergreenPhases(account, bundle, subscription);
-        verifyCancellationTransition(account, bundle, subscription);
-        verifySystemCancellationTransition(account, bundle, subscription);
+        verifyCancellationTransition(account, bundle);
+        verifySystemCancellationTransition(account, bundle);
     }
 
     private void verifyTrialAndEvergreenPhases(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws CatalogApiException {
@@ -365,7 +366,7 @@ public class TestAnalytics extends TestIntegrationBase {
         Assert.assertEquals(futureTransition.getNextSubscription().getSubscriptionId(), subscription.getId());
     }
 
-    private void verifyCancellationTransition(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws CatalogApiException {
+    private void verifyCancellationTransition(final Account account, final SubscriptionBundle bundle) throws CatalogApiException {
         final Product currentProduct = subscriptionPlan.getProduct();
         final List<BusinessSubscriptionTransition> transitions = analyticsUserApi.getTransitionsForBundle(bundle.getKey());
 
@@ -380,16 +381,13 @@ public class TestAnalytics extends TestIntegrationBase {
         Assert.assertEquals(cancellationRequest.getPreviousSubscription(), transitions.get(1).getNextSubscription());
     }
 
-    private void verifySystemCancellationTransition(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws CatalogApiException {
-        final Plan currentPlan = subscription.getCurrentPlan();
-        final Product currentProduct = currentPlan.getProduct();
-
+    private void verifySystemCancellationTransition(final Account account, final SubscriptionBundle bundle) throws CatalogApiException {
         final List<BusinessSubscriptionTransition> transitions = analyticsUserApi.getTransitionsForBundle(bundle.getKey());
 
         final BusinessSubscriptionTransition systemCancellation = transitions.get(3);
         Assert.assertEquals(systemCancellation.getExternalKey(), bundle.getKey());
         Assert.assertEquals(systemCancellation.getAccountKey(), account.getExternalKey());
-        Assert.assertEquals(systemCancellation.getEvent().getCategory(), currentProduct.getCategory());
+        Assert.assertEquals(systemCancellation.getEvent().getCategory(), ProductCategory.BASE);
         Assert.assertEquals(systemCancellation.getEvent().getEventType(), BusinessSubscriptionEvent.EventType.SYSTEM_CANCEL);
 
         Assert.assertNull(systemCancellation.getNextSubscription());