killbill-memoizeit

analytics: add bbs table The bbs table is used as a summary

4/17/2013 4:49:15 PM

Changes

Details

diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
index c04962f..2ded659 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
@@ -63,7 +63,7 @@ public class AnalyticsListener implements OSGIKillbillEventHandler {
         this.logService = logService;
 
         this.bacDao = new BusinessAccountDao(logService, osgiKillbillAPI, osgiKillbillDataSource);
-        this.bstDao = new BusinessSubscriptionTransitionDao(logService, osgiKillbillAPI, osgiKillbillDataSource);
+        this.bstDao = new BusinessSubscriptionTransitionDao(logService, osgiKillbillAPI, osgiKillbillDataSource, bacDao);
         this.binDao = new BusinessInvoiceDao(logService, osgiKillbillAPI, osgiKillbillDataSource, bacDao);
         this.bipDao = new BusinessInvoicePaymentDao(logService, osgiKillbillAPI, osgiKillbillDataSource, bacDao, binDao);
         this.bosDao = new BusinessOverdueStatusDao(logService, osgiKillbillAPI, osgiKillbillDataSource);
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/api/user/AnalyticsUserApi.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/api/user/AnalyticsUserApi.java
index c4dfdb5..f3d573c 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/api/user/AnalyticsUserApi.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/api/user/AnalyticsUserApi.java
@@ -63,7 +63,7 @@ public class AnalyticsUserApi extends BusinessAnalyticsBase {
         super(logService, osgiKillbillAPI);
         this.analyticsDao = new AnalyticsDao(osgiKillbillAPI, osgiKillbillDataSource);
         this.bacDao = new BusinessAccountDao(logService, osgiKillbillAPI, osgiKillbillDataSource);
-        this.bstDao = new BusinessSubscriptionTransitionDao(logService, osgiKillbillAPI, osgiKillbillDataSource);
+        this.bstDao = new BusinessSubscriptionTransitionDao(logService, osgiKillbillAPI, osgiKillbillDataSource, bacDao);
         this.binDao = new BusinessInvoiceDao(logService, osgiKillbillAPI, osgiKillbillDataSource, bacDao);
         this.bipDao = new BusinessInvoicePaymentDao(logService, osgiKillbillAPI, osgiKillbillDataSource, bacDao, binDao);
         this.bosDao = new BusinessOverdueStatusDao(logService, osgiKillbillAPI, osgiKillbillDataSource);
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessAnalyticsBase.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessAnalyticsBase.java
index 2bf4f95..6e50c88 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessAnalyticsBase.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/BusinessAnalyticsBase.java
@@ -169,6 +169,22 @@ public abstract class BusinessAnalyticsBase {
         return entitlementUserApi.getBundlesForAccount(accountId, context);
     }
 
+    protected Long getBundleRecordId(final UUID bundleId, final TenantContext context) throws AnalyticsRefreshException {
+        final RecordIdApi recordIdUserApi = getRecordIdUserApi();
+        return recordIdUserApi.getRecordId(bundleId, ObjectType.BUNDLE, context);
+    }
+
+    protected AuditLog getBundleCreationAuditLog(final UUID bundleId, final TenantContext context) throws AnalyticsRefreshException {
+        final List<AuditLog> auditLogsForBundle = getAuditUserApi().getAuditLogs(bundleId, ObjectType.BUNDLE, AuditLevel.MINIMAL, context);
+        for (final AuditLog auditLog : auditLogsForBundle) {
+            if (auditLog.getChangeType().equals(ChangeType.INSERT)) {
+                return auditLog;
+            }
+        }
+
+        throw new AnalyticsRefreshException("Unable to find Bundle creation audit log for id " + bundleId);
+    }
+
     protected Subscription getSubscription(final UUID subscriptionId, final TenantContext context) throws AnalyticsRefreshException {
         final EntitlementUserApi entitlementUserApi = getEntitlementUserApi();
 
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessAnalyticsSqlDao.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessAnalyticsSqlDao.java
index 1b61b0b..94725f4 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessAnalyticsSqlDao.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessAnalyticsSqlDao.java
@@ -28,6 +28,7 @@ import com.ning.billing.commons.jdbi.binder.SmartBindBean;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountFieldModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountTagModelDao;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessBundleSummaryModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceAdjustmentModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceFieldModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceItemAdjustmentModelDao;
@@ -72,6 +73,11 @@ public interface BusinessAnalyticsSqlDao extends Transactional<BusinessAnalytics
                                                                                                     final TenantContext tenantContext);
 
     @SqlQuery
+    public List<BusinessBundleSummaryModelDao> getBundleSummariesByAccountRecordId(@Bind("accountRecordId") final Long accountRecordId,
+                                                                                   @Bind("tenantRecordId") final Long tenantRecordId,
+                                                                                   final TenantContext tenantContext);
+
+    @SqlQuery
     public List<BusinessOverdueStatusModelDao> getOverdueStatusesByAccountRecordId(@Bind("accountRecordId") final Long accountRecordId,
                                                                                    @Bind("tenantRecordId") final Long tenantRecordId,
                                                                                    final TenantContext tenantContext);
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessBundleSummaryDao.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessBundleSummaryDao.java
new file mode 100644
index 0000000..e49a686
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessBundleSummaryDao.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.dao;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.UUID;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.osgi.bundles.analytics.AnalyticsRefreshException;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessBundleSummaryModelDao;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessModelDaoBase.ReportGroup;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscriptionTransitionModelDao;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillAPI;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillDataSource;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+
+public class BusinessBundleSummaryDao extends BusinessAnalyticsDaoBase {
+
+    public BusinessBundleSummaryDao(final OSGIKillbillLogService logService,
+                                    final OSGIKillbillAPI osgiKillbillAPI,
+                                    final OSGIKillbillDataSource osgiKillbillDataSource) {
+        super(logService, osgiKillbillAPI, osgiKillbillDataSource);
+    }
+
+    public void updateInTransaction(final Collection<BusinessBundleSummaryModelDao> bbss,
+                                    final Long accountRecordId,
+                                    final Long tenantRecordId,
+                                    final BusinessAnalyticsSqlDao transactional,
+                                    final CallContext context) {
+        transactional.deleteByAccountRecordId(BusinessBundleSummaryModelDao.BUNDLE_SUMMARIES_TABLE_NAME,
+                                              accountRecordId,
+                                              tenantRecordId,
+                                              context);
+
+        for (final BusinessBundleSummaryModelDao bbs : bbss) {
+            transactional.create(bbs.getTableName(), bbs, context);
+        }
+
+        // The update of summary columns in BAC will be done via BST
+    }
+
+    public Collection<BusinessBundleSummaryModelDao> createBusinessBundleSummaries(final Account account,
+                                                                                   final Long accountRecordId,
+                                                                                   final Collection<BusinessSubscriptionTransitionModelDao> bsts,
+                                                                                   final Long tenantRecordId,
+                                                                                   final ReportGroup reportGroup,
+                                                                                   final CallContext context) throws AnalyticsRefreshException {
+        final Map<UUID, Integer> rankForBundle = new LinkedHashMap<UUID, Integer>();
+        final Map<UUID, BusinessSubscriptionTransitionModelDao> bstForBundle = new LinkedHashMap<UUID, BusinessSubscriptionTransitionModelDao>();
+        filterBstsForBasePlans(bsts, rankForBundle, bstForBundle);
+
+        final Collection<BusinessBundleSummaryModelDao> bbss = new LinkedList<BusinessBundleSummaryModelDao>();
+        for (final BusinessSubscriptionTransitionModelDao bst : bstForBundle.values()) {
+            final BusinessBundleSummaryModelDao bbs = buildBBS(account,
+                                                               accountRecordId,
+                                                               bst,
+                                                               rankForBundle.get(bst.getBundleId()),
+                                                               tenantRecordId,
+                                                               reportGroup,
+                                                               context);
+            bbss.add(bbs);
+        }
+        return bbss;
+    }
+
+    @VisibleForTesting
+    void filterBstsForBasePlans(final Collection<BusinessSubscriptionTransitionModelDao> bsts, final Map<UUID, Integer> rankForBundle, final Map<UUID, BusinessSubscriptionTransitionModelDao> bstForBundle) {// Find bsts for BASE subscriptions only and sort them using the next start date
+        final Collection<BusinessSubscriptionTransitionModelDao> sortedBundlesBst = Ordering.from(new Comparator<BusinessSubscriptionTransitionModelDao>() {
+            @Override
+            public int compare(final BusinessSubscriptionTransitionModelDao o1, final BusinessSubscriptionTransitionModelDao o2) {
+                return o1.getNextStartDate().compareTo(o2.getNextStartDate());
+            }
+        }).sortedCopy(Iterables.filter(bsts, new Predicate<BusinessSubscriptionTransitionModelDao>() {
+            @Override
+            public boolean apply(final BusinessSubscriptionTransitionModelDao input) {
+                return ProductCategory.BASE.toString().equals(input.getNextProductCategory());
+            }
+        }));
+
+        UUID lastBundleId = null;
+        Integer lastBundleRank = 0;
+        for (final BusinessSubscriptionTransitionModelDao bst : sortedBundlesBst) {
+            // Note that sortedBundlesBst is not ordered bundle by bundle, i.e. we may have:
+            // bundleId1 CREATE, bundleId2 CREATE, bundleId1 PHASE, bundleId3 CREATE bundleId2 PHASE
+            if (lastBundleId == null || (!lastBundleId.equals(bst.getBundleId()) && rankForBundle.get(bst.getBundleId()) == null)) {
+                lastBundleRank++;
+                lastBundleId = bst.getBundleId();
+                rankForBundle.put(lastBundleId, lastBundleRank);
+            }
+
+            if (bstForBundle.get(bst.getBundleId()) == null ||
+                bstForBundle.get(bst.getBundleId()).getNextStartDate().isBefore(bst.getNextStartDate())) {
+                bstForBundle.put(bst.getBundleId(), bst);
+            }
+        }
+    }
+
+    private BusinessBundleSummaryModelDao buildBBS(final Account account, final Long accountRecordId, final BusinessSubscriptionTransitionModelDao bst, final Integer bundleAccountRank, final Long tenantRecordId, final ReportGroup reportGroup, final CallContext context) throws AnalyticsRefreshException {
+        final SubscriptionBundle bundle = getSubscriptionBundle(bst.getBundleId(), context);
+        final Long bundleRecordId = getBundleRecordId(bundle.getId(), context);
+        final AuditLog creationAuditLog = getBundleCreationAuditLog(bundle.getId(), context);
+
+        return new BusinessBundleSummaryModelDao(account,
+                                                 accountRecordId,
+                                                 bundle,
+                                                 bundleRecordId,
+                                                 bundleAccountRank,
+                                                 bst,
+                                                 creationAuditLog,
+                                                 tenantRecordId,
+                                                 reportGroup);
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessDBIProvider.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessDBIProvider.java
index 75b8ab9..7c8cf5d 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessDBIProvider.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessDBIProvider.java
@@ -28,6 +28,7 @@ import com.ning.billing.commons.jdbi.mapper.LowerToCamelBeanMapperFactory;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountFieldModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountTagModelDao;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessBundleSummaryModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceAdjustmentModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceFieldModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceItemAdjustmentModelDao;
@@ -69,6 +70,7 @@ public class BusinessDBIProvider {
         dbi.registerMapper(new LowerToCamelBeanMapperFactory(BusinessInvoiceTagModelDao.class));
         dbi.registerMapper(new LowerToCamelBeanMapperFactory(BusinessOverdueStatusModelDao.class));
         dbi.registerMapper(new LowerToCamelBeanMapperFactory(BusinessSubscriptionTransitionModelDao.class));
+        dbi.registerMapper(new LowerToCamelBeanMapperFactory(BusinessBundleSummaryModelDao.class));
 
         dbi.setStatementLocator(new AnalyticsStatementLocator());
 
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessSubscriptionTransitionDao.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessSubscriptionTransitionDao.java
index 9749964..b3fda1c 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessSubscriptionTransitionDao.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/BusinessSubscriptionTransitionDao.java
@@ -31,6 +31,8 @@ import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.entitlement.api.user.SubscriptionTransition;
 import com.ning.billing.osgi.bundles.analytics.AnalyticsRefreshException;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountModelDao;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessBundleSummaryModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessModelDaoBase.ReportGroup;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscription;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscriptionEvent;
@@ -43,41 +45,79 @@ import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
 
 public class BusinessSubscriptionTransitionDao extends BusinessAnalyticsDaoBase {
 
+    private final BusinessAccountDao businessAccountDao;
+    private final BusinessBundleSummaryDao businessBundleSummaryDao;
+
     public BusinessSubscriptionTransitionDao(final OSGIKillbillLogService logService,
                                              final OSGIKillbillAPI osgiKillbillAPI,
-                                             final OSGIKillbillDataSource osgiKillbillDataSource) {
+                                             final OSGIKillbillDataSource osgiKillbillDataSource,
+                                             final BusinessAccountDao businessAccountDao) {
         super(logService, osgiKillbillAPI, osgiKillbillDataSource);
+        this.businessAccountDao = businessAccountDao;
+        this.businessBundleSummaryDao = new BusinessBundleSummaryDao(logService, osgiKillbillAPI, osgiKillbillDataSource);
     }
 
     public void update(final UUID accountId, final CallContext context) throws AnalyticsRefreshException {
         final Account account = getAccount(accountId, context);
+        final ReportGroup reportGroup = getReportGroup(account.getId(), context);
 
-        // Recompute all invoices and invoice items
-        final Collection<BusinessSubscriptionTransitionModelDao> bsts = createBusinessSubscriptionTransitions(account, context);
+        // Recompute the account record
+        final BusinessAccountModelDao bac = businessAccountDao.createBusinessAccount(account, context);
 
+        // Recompute all invoices and invoice items
+        final Collection<BusinessSubscriptionTransitionModelDao> bsts = createBusinessSubscriptionTransitions(account,
+                                                                                                              bac.getAccountRecordId(),
+                                                                                                              bac.getTenantRecordId(),
+                                                                                                              reportGroup,
+                                                                                                              context);
+
+        // Recompute the bundle summary records
+        final Collection<BusinessBundleSummaryModelDao> bbss = businessBundleSummaryDao.createBusinessBundleSummaries(account,
+                                                                                                                      bac.getAccountRecordId(),
+                                                                                                                      bsts,
+                                                                                                                      bac.getTenantRecordId(),
+                                                                                                                      reportGroup,
+                                                                                                                      context);
         sqlDao.inTransaction(new Transaction<Void, BusinessAnalyticsSqlDao>() {
             @Override
             public Void inTransaction(final BusinessAnalyticsSqlDao transactional, final TransactionStatus status) throws Exception {
-                updateInTransaction(bsts, transactional, context);
+                updateInTransaction(bac, bbss, bsts, transactional, context);
                 return null;
             }
         });
     }
 
-    public void updateInTransaction(final Collection<BusinessSubscriptionTransitionModelDao> bsts, final BusinessAnalyticsSqlDao transactional, final CallContext context) {
-        if (bsts.size() == 0) {
-            return;
-        }
-
-        final BusinessSubscriptionTransitionModelDao firstBst = bsts.iterator().next();
-        transactional.deleteByAccountRecordId(firstBst.getTableName(), firstBst.getAccountRecordId(), firstBst.getTenantRecordId(), context);
+    public void updateInTransaction(final BusinessAccountModelDao bac,
+                                    final Collection<BusinessBundleSummaryModelDao> bbss,
+                                    final Collection<BusinessSubscriptionTransitionModelDao> bsts,
+                                    final BusinessAnalyticsSqlDao transactional,
+                                    final CallContext context) {
+        // Update the subscription transitions
+        transactional.deleteByAccountRecordId(BusinessSubscriptionTransitionModelDao.SUBSCRIPTION_TABLE_NAME,
+                                              bac.getAccountRecordId(),
+                                              bac.getTenantRecordId(),
+                                              context);
 
         for (final BusinessSubscriptionTransitionModelDao bst : bsts) {
             transactional.create(bst.getTableName(), bst, context);
         }
+
+        // Update the summary table per bundle
+        businessBundleSummaryDao.updateInTransaction(bbss,
+                                                     bac.getAccountRecordId(),
+                                                     bac.getTenantRecordId(),
+                                                     transactional,
+                                                     context);
+
+        // Update BAC
+        businessAccountDao.updateInTransaction(bac, transactional, context);
     }
 
-    private Collection<BusinessSubscriptionTransitionModelDao> createBusinessSubscriptionTransitions(final Account account, final CallContext context) throws AnalyticsRefreshException {
+    private Collection<BusinessSubscriptionTransitionModelDao> createBusinessSubscriptionTransitions(final Account account,
+                                                                                                     final Long accountRecordId,
+                                                                                                     final Long tenantRecordId,
+                                                                                                     @Nullable final ReportGroup reportGroup,
+                                                                                                     final CallContext context) throws AnalyticsRefreshException {
         final Collection<BusinessSubscriptionTransitionModelDao> bsts = new LinkedList<BusinessSubscriptionTransitionModelDao>();
 
         final List<SubscriptionBundle> bundles = getSubscriptionBundlesForAccount(account.getId(), context);
@@ -92,10 +132,13 @@ public class BusinessSubscriptionTransitionDao extends BusinessAnalyticsDaoBase 
                 for (final SubscriptionTransition transition : transitions) {
                     final BusinessSubscription nextSubscription = getBusinessSubscriptionFromTransition(account, transition);
                     final BusinessSubscriptionTransitionModelDao bst = createBusinessSubscriptionTransition(account,
+                                                                                                            accountRecordId,
                                                                                                             bundle,
                                                                                                             transition,
                                                                                                             prevNextSubscription,
                                                                                                             nextSubscription,
+                                                                                                            tenantRecordId,
+                                                                                                            reportGroup,
                                                                                                             context);
                     if (bst != null) {
                         bsts.add(bst);
@@ -109,10 +152,13 @@ public class BusinessSubscriptionTransitionDao extends BusinessAnalyticsDaoBase 
     }
 
     private BusinessSubscriptionTransitionModelDao createBusinessSubscriptionTransition(final Account account,
+                                                                                        final Long accountRecordId,
                                                                                         final SubscriptionBundle subscriptionBundle,
                                                                                         final SubscriptionTransition subscriptionTransition,
                                                                                         @Nullable final BusinessSubscription prevNextSubscription,
                                                                                         final BusinessSubscription nextSubscription,
+                                                                                        final Long tenantRecordId,
+                                                                                        @Nullable final ReportGroup reportGroup,
                                                                                         final CallContext context) throws AnalyticsRefreshException {
         final BusinessSubscriptionEvent businessEvent = BusinessSubscriptionEvent.fromTransition(subscriptionTransition);
         if (businessEvent == null) {
@@ -122,10 +168,6 @@ public class BusinessSubscriptionTransitionDao extends BusinessAnalyticsDaoBase 
         final Long subscriptionEventRecordId = getSubscriptionEventRecordId(subscriptionTransition.getNextEventId(), context);
         final AuditLog creationAuditLog = getSubscriptionEventCreationAuditLog(subscriptionTransition.getNextEventId(), context);
 
-        final Long accountRecordId = getAccountRecordId(account.getId(), context);
-        final Long tenantRecordId = getTenantRecordId(context);
-        final ReportGroup reportGroup = getReportGroup(account.getId(), context);
-
         return new BusinessSubscriptionTransitionModelDao(account,
                                                           accountRecordId,
                                                           subscriptionBundle,
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessBundleSummaryModelDao.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessBundleSummaryModelDao.java
new file mode 100644
index 0000000..535d09c
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessBundleSummaryModelDao.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.dao.model;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.util.audit.AuditLog;
+
+public class BusinessBundleSummaryModelDao extends BusinessModelDaoBase {
+
+    public static final String BUNDLE_SUMMARIES_TABLE_NAME = "bbs";
+
+    private Long bundleRecordId;
+    private UUID bundleId;
+    private String bundleExternalKey;
+    private UUID subscriptionId;
+    private Integer bundleAccountRank;
+    private String currentProductName;
+    private String currentProductType;
+    private String currentProductCategory;
+    private String currentSlug;
+    private String currentPhase;
+    private String currentBillingPeriod;
+    private BigDecimal currentPrice;
+    private String currentPriceList;
+    private BigDecimal currentMrr;
+    private String currentCurrency;
+    private Boolean currentBusinessActive;
+    private DateTime currentStartDate;
+    private DateTime currentEndDate;
+    private String currentState;
+
+    public BusinessBundleSummaryModelDao() { /* When reading from the database */ }
+
+    public BusinessBundleSummaryModelDao(final Long bundleRecordId,
+                                         final UUID bundleId,
+                                         final String bundleExternalKey,
+                                         final UUID subscriptionId,
+                                         final Integer bundleAccountRank,
+                                         final BusinessSubscriptionTransitionModelDao bst,
+                                         final DateTime createdDate,
+                                         final String createdBy,
+                                         final String createdReasonCode,
+                                         final String createdComments,
+                                         final UUID accountId,
+                                         final String accountName,
+                                         final String accountExternalKey,
+                                         final Long accountRecordId,
+                                         final Long tenantRecordId,
+                                         @Nullable final ReportGroup reportGroup)
+
+    {
+        super(createdDate,
+              createdBy,
+              createdReasonCode,
+              createdComments,
+              accountId,
+              accountName,
+              accountExternalKey,
+              accountRecordId,
+              tenantRecordId,
+              reportGroup);
+        this.bundleRecordId = bundleRecordId;
+        this.bundleId = bundleId;
+        this.bundleExternalKey = bundleExternalKey;
+        this.subscriptionId = subscriptionId;
+        this.bundleAccountRank = bundleAccountRank;
+        this.currentProductName = bst.getNextProductName();
+        this.currentProductType = bst.getNextProductType();
+        this.currentProductCategory = bst.getNextProductCategory();
+        this.currentSlug = bst.getNextSlug();
+        this.currentPhase = bst.getNextPhase();
+        this.currentBillingPeriod = bst.getNextBillingPeriod();
+        this.currentPrice = bst.getNextPrice();
+        this.currentPriceList = bst.getNextPriceList();
+        this.currentMrr = bst.getNextMrr();
+        this.currentCurrency = bst.getNextCurrency();
+        this.currentBusinessActive = bst.getNextBusinessActive();
+        this.currentStartDate = bst.getNextStartDate();
+        this.currentEndDate = bst.getNextEndDate();
+        this.currentState = bst.getNextState();
+    }
+
+
+    public BusinessBundleSummaryModelDao(final Account account,
+                                         final Long accountRecordId,
+                                         final SubscriptionBundle bundle,
+                                         final Long bundleRecordId,
+                                         final Integer bundleAccountRank,
+                                         final BusinessSubscriptionTransitionModelDao bst,
+                                         final AuditLog creationAuditLog,
+                                         final Long tenantRecordId,
+                                         @Nullable final ReportGroup reportGroup) {
+        this(bundleRecordId,
+             bundle.getId(),
+             bundle.getExternalKey(),
+             bst.getSubscriptionId(),
+             bundleAccountRank,
+             bst,
+             bundle.getCreatedDate(),
+             creationAuditLog.getUserName(),
+             creationAuditLog.getReasonCode(),
+             creationAuditLog.getComment(),
+             account.getId(),
+             account.getName(),
+             account.getExternalKey(),
+             accountRecordId,
+             tenantRecordId,
+             reportGroup);
+    }
+
+    @Override
+    public String getTableName() {
+        return BUNDLE_SUMMARIES_TABLE_NAME;
+    }
+
+    public Long getBundleRecordId() {
+        return bundleRecordId;
+    }
+
+    public UUID getBundleId() {
+        return bundleId;
+    }
+
+    public String getBundleExternalKey() {
+        return bundleExternalKey;
+    }
+
+    public UUID getSubscriptionId() {
+        return subscriptionId;
+    }
+
+    public Integer getBundleAccountRank() {
+        return bundleAccountRank;
+    }
+
+    public String getCurrentProductName() {
+        return currentProductName;
+    }
+
+    public String getCurrentProductType() {
+        return currentProductType;
+    }
+
+    public String getCurrentProductCategory() {
+        return currentProductCategory;
+    }
+
+    public String getCurrentSlug() {
+        return currentSlug;
+    }
+
+    public String getCurrentPhase() {
+        return currentPhase;
+    }
+
+    public String getCurrentBillingPeriod() {
+        return currentBillingPeriod;
+    }
+
+    public BigDecimal getCurrentPrice() {
+        return currentPrice;
+    }
+
+    public String getCurrentPriceList() {
+        return currentPriceList;
+    }
+
+    public BigDecimal getCurrentMrr() {
+        return currentMrr;
+    }
+
+    public String getCurrentCurrency() {
+        return currentCurrency;
+    }
+
+    public Boolean getCurrentBusinessActive() {
+        return currentBusinessActive;
+    }
+
+    public DateTime getCurrentStartDate() {
+        return currentStartDate;
+    }
+
+    public DateTime getCurrentEndDate() {
+        return currentEndDate;
+    }
+
+    public String getCurrentState() {
+        return currentState;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("BusinessBundleSummaryModelDao{");
+        sb.append("bundleRecordId=").append(bundleRecordId);
+        sb.append(", bundleId=").append(bundleId);
+        sb.append(", bundleExternalKey='").append(bundleExternalKey).append('\'');
+        sb.append(", subscriptionId=").append(subscriptionId);
+        sb.append(", bundleAccountRank=").append(bundleAccountRank);
+        sb.append(", currentProductName='").append(currentProductName).append('\'');
+        sb.append(", currentProductType='").append(currentProductType).append('\'');
+        sb.append(", currentProductCategory='").append(currentProductCategory).append('\'');
+        sb.append(", currentSlug='").append(currentSlug).append('\'');
+        sb.append(", currentPhase='").append(currentPhase).append('\'');
+        sb.append(", currentBillingPeriod='").append(currentBillingPeriod).append('\'');
+        sb.append(", currentPrice=").append(currentPrice);
+        sb.append(", currentPriceList='").append(currentPriceList).append('\'');
+        sb.append(", currentMrr=").append(currentMrr);
+        sb.append(", currentCurrency='").append(currentCurrency).append('\'');
+        sb.append(", currentBusinessActive=").append(currentBusinessActive);
+        sb.append(", currentStartDate=").append(currentStartDate);
+        sb.append(", currentEndDate=").append(currentEndDate);
+        sb.append(", currentState='").append(currentState).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        final BusinessBundleSummaryModelDao that = (BusinessBundleSummaryModelDao) o;
+
+        if (bundleAccountRank != null ? !bundleAccountRank.equals(that.bundleAccountRank) : that.bundleAccountRank != null) {
+            return false;
+        }
+        if (bundleExternalKey != null ? !bundleExternalKey.equals(that.bundleExternalKey) : that.bundleExternalKey != null) {
+            return false;
+        }
+        if (bundleId != null ? !bundleId.equals(that.bundleId) : that.bundleId != null) {
+            return false;
+        }
+        if (bundleRecordId != null ? !bundleRecordId.equals(that.bundleRecordId) : that.bundleRecordId != null) {
+            return false;
+        }
+        if (currentBillingPeriod != null ? !currentBillingPeriod.equals(that.currentBillingPeriod) : that.currentBillingPeriod != null) {
+            return false;
+        }
+        if (currentBusinessActive != null ? !currentBusinessActive.equals(that.currentBusinessActive) : that.currentBusinessActive != null) {
+            return false;
+        }
+        if (currentCurrency != null ? !currentCurrency.equals(that.currentCurrency) : that.currentCurrency != null) {
+            return false;
+        }
+        if (currentEndDate != null ? !currentEndDate.equals(that.currentEndDate) : that.currentEndDate != null) {
+            return false;
+        }
+        if (currentMrr != null ? !(currentMrr.compareTo(that.currentMrr) == 0) : that.currentMrr != null) {
+            return false;
+        }
+        if (currentPhase != null ? !currentPhase.equals(that.currentPhase) : that.currentPhase != null) {
+            return false;
+        }
+        if (currentPrice != null ? !(currentPrice.compareTo(that.currentPrice) == 0) : that.currentPrice != null) {
+            return false;
+        }
+        if (currentPriceList != null ? !currentPriceList.equals(that.currentPriceList) : that.currentPriceList != null) {
+            return false;
+        }
+        if (currentProductCategory != null ? !currentProductCategory.equals(that.currentProductCategory) : that.currentProductCategory != null) {
+            return false;
+        }
+        if (currentProductName != null ? !currentProductName.equals(that.currentProductName) : that.currentProductName != null) {
+            return false;
+        }
+        if (currentProductType != null ? !currentProductType.equals(that.currentProductType) : that.currentProductType != null) {
+            return false;
+        }
+        if (currentSlug != null ? !currentSlug.equals(that.currentSlug) : that.currentSlug != null) {
+            return false;
+        }
+        if (currentStartDate != null ? !currentStartDate.equals(that.currentStartDate) : that.currentStartDate != null) {
+            return false;
+        }
+        if (currentState != null ? !currentState.equals(that.currentState) : that.currentState != null) {
+            return false;
+        }
+        if (subscriptionId != null ? !subscriptionId.equals(that.subscriptionId) : that.subscriptionId != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (bundleRecordId != null ? bundleRecordId.hashCode() : 0);
+        result = 31 * result + (bundleId != null ? bundleId.hashCode() : 0);
+        result = 31 * result + (bundleExternalKey != null ? bundleExternalKey.hashCode() : 0);
+        result = 31 * result + (subscriptionId != null ? subscriptionId.hashCode() : 0);
+        result = 31 * result + (bundleAccountRank != null ? bundleAccountRank.hashCode() : 0);
+        result = 31 * result + (currentProductName != null ? currentProductName.hashCode() : 0);
+        result = 31 * result + (currentProductType != null ? currentProductType.hashCode() : 0);
+        result = 31 * result + (currentProductCategory != null ? currentProductCategory.hashCode() : 0);
+        result = 31 * result + (currentSlug != null ? currentSlug.hashCode() : 0);
+        result = 31 * result + (currentPhase != null ? currentPhase.hashCode() : 0);
+        result = 31 * result + (currentBillingPeriod != null ? currentBillingPeriod.hashCode() : 0);
+        result = 31 * result + (currentPrice != null ? currentPrice.hashCode() : 0);
+        result = 31 * result + (currentPriceList != null ? currentPriceList.hashCode() : 0);
+        result = 31 * result + (currentMrr != null ? currentMrr.hashCode() : 0);
+        result = 31 * result + (currentCurrency != null ? currentCurrency.hashCode() : 0);
+        result = 31 * result + (currentBusinessActive != null ? currentBusinessActive.hashCode() : 0);
+        result = 31 * result + (currentStartDate != null ? currentStartDate.hashCode() : 0);
+        result = 31 * result + (currentEndDate != null ? currentEndDate.hashCode() : 0);
+        result = 31 * result + (currentState != null ? currentState.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessSubscriptionTransitionModelDao.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessSubscriptionTransitionModelDao.java
index 1567f9b..346f837 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessSubscriptionTransitionModelDao.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/dao/model/BusinessSubscriptionTransitionModelDao.java
@@ -33,13 +33,15 @@ import com.ning.billing.util.audit.AuditLog;
  */
 public class BusinessSubscriptionTransitionModelDao extends BusinessModelDaoBase {
 
-    private static final String SUBSCRIPTION_TABLE_NAME = "bst";
+    public static final String SUBSCRIPTION_TABLE_NAME = "bst";
+
     private Long subscriptionEventRecordId;
     private UUID bundleId;
     private String bundleExternalKey;
     private UUID subscriptionId;
     private DateTime requestedTimestamp;
     private String event;
+
     private String prevProductName;
     private String prevProductType;
     private String prevProductCategory;
@@ -53,6 +55,7 @@ public class BusinessSubscriptionTransitionModelDao extends BusinessModelDaoBase
     private Boolean prevBusinessActive;
     private DateTime prevStartDate;
     private String prevState;
+
     private String nextProductName;
     private String nextProductType;
     private String nextProductCategory;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/com/ning/billing/osgi/bundles/analytics/dao/BusinessAnalyticsSqlDao.sql.stg b/osgi-bundles/bundles/analytics/src/main/resources/com/ning/billing/osgi/bundles/analytics/dao/BusinessAnalyticsSqlDao.sql.stg
index 8d829d4..e5089cb 100644
--- a/osgi-bundles/bundles/analytics/src/main/resources/com/ning/billing/osgi/bundles/analytics/dao/BusinessAnalyticsSqlDao.sql.stg
+++ b/osgi-bundles/bundles/analytics/src/main/resources/com/ning/billing/osgi/bundles/analytics/dao/BusinessAnalyticsSqlDao.sql.stg
@@ -92,6 +92,70 @@ insert into bst (
 );
 >>
 
+createBbs() ::= <<
+insert into bbs (
+  bundle_record_id
+, bundle_id
+, bundle_external_key
+, subscription_id
+, bundle_account_rank
+, current_product_name
+, current_product_type
+, current_product_category
+, current_slug
+, current_phase
+, current_billing_period
+, current_price
+, current_price_list
+, current_mrr
+, current_currency
+, current_state
+, current_business_active
+, current_start_date
+, current_end_date
+, created_date
+, created_by
+, created_reason_code
+, created_comments
+, account_id
+, account_name
+, account_external_key
+, account_record_id
+, tenant_record_id
+, report_group
+) values (
+  :bundleRecordId
+, :bundleId
+, :bundleExternalKey
+, :subscriptionId
+, :bundleAccountRank
+, :currentProductName
+, :currentProductType
+, :currentProductCategory
+, :currentSlug
+, :currentPhase
+, :currentBillingPeriod
+, :currentPrice
+, :currentPriceList
+, :currentMrr
+, :currentCurrency
+, :currentState
+, :currentBusinessActive
+, :currentStartDate
+, :currentEndDate
+, :createdDate
+, :createdBy
+, :createdReasonCode
+, :createdComments
+, :accountId
+, :accountName
+, :accountExternalKey
+, :accountRecordId
+, :tenantRecordId
+, :reportGroup
+);
+>>
+
 createBac() ::= <<
 insert into bac (
   email
@@ -1109,6 +1173,11 @@ getSubscriptionTransitionsByAccountRecordId() ::= <<
 ;
 >>
 
+getBundleSummariesByAccountRecordId() ::= <<
+<SELECT_STAR_FROM_TABLE("bbs")>
+;
+>>
+
 getOverdueStatusesByAccountRecordId() ::= <<
 <SELECT_STAR_FROM_TABLE("bos")>
 ;
diff --git a/osgi-bundles/bundles/analytics/src/main/resources/com/ning/billing/osgi/bundles/analytics/ddl.sql b/osgi-bundles/bundles/analytics/src/main/resources/com/ning/billing/osgi/bundles/analytics/ddl.sql
index b1bbbf3..1dca26c 100644
--- a/osgi-bundles/bundles/analytics/src/main/resources/com/ning/billing/osgi/bundles/analytics/ddl.sql
+++ b/osgi-bundles/bundles/analytics/src/main/resources/com/ning/billing/osgi/bundles/analytics/ddl.sql
@@ -54,6 +54,47 @@ create index bst_account_id on bst(account_id);
 create index bst_account_record_id on bst(account_record_id);
 create index bst_tenant_account_record_id on bst(tenant_record_id, account_record_id);
 
+-- Bundle summary
+drop table if exists bbs;
+create table bbs (
+  record_id int(11) unsigned not null auto_increment
+, bundle_record_id int(11) unsigned default null
+, bundle_id char(36) not null
+, bundle_external_key varchar(50) not null
+, subscription_id char(36) not null
+, bundle_account_rank int(11) not null
+, charged_through_date datetime default null
+, current_product_name varchar(50) default null
+, current_product_type varchar(50) default null
+, current_product_category varchar(50) default null
+, current_slug varchar(50) default null
+, current_phase varchar(50) default null
+, current_billing_period varchar(50) default null
+, current_price numeric(10, 4) default 0
+, current_price_list varchar(50) default null
+, current_mrr numeric(10, 4) default 0
+, current_currency varchar(50) default null
+, current_state varchar(50) default null
+, current_business_active bool default true
+, current_start_date datetime default null
+, current_end_date datetime default null
+, created_date datetime not null
+, created_by varchar(50) not null
+, created_reason_code varchar(255) default null
+, created_comments varchar(255) default null
+, account_id char(36) not null
+, account_name varchar(100) not null
+, account_external_key varchar(50) not null
+, account_record_id int(11) unsigned default null
+, tenant_record_id int(11) unsigned default null
+, report_group enum('default', 'test', 'partner') not null
+, primary key(record_id)
+);
+create index bbs_bundle_external_key on bbs(bundle_external_key);
+create index bbs_account_id on bbs(account_id);
+create index bbs_account_record_id on bbs(account_record_id);
+create index bbs_tenant_account_record_id on bbs(tenant_record_id, account_record_id);
+
 -- Accounts
 drop table if exists bac;
 create table bac (
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/AnalyticsTestSuiteNoDB.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/AnalyticsTestSuiteNoDB.java
index fd1d2a3..3b336e4 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/AnalyticsTestSuiteNoDB.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/AnalyticsTestSuiteNoDB.java
@@ -87,6 +87,7 @@ public abstract class AnalyticsTestSuiteNoDB {
     protected final Long fieldRecordId = 7L;
     protected final Long tagRecordId = 8L;
     protected final Long tenantRecordId = 9L;
+    protected final Long bundleRecordId = 10L;
 
     protected final ReportGroup reportGroup = ReportGroup.partner;
     protected final BusinessInvoiceItemType invoiceItemType = BusinessInvoiceItemType.INVOICE_ITEM_ADJUSTMENT;
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/model/TestBusinessBundleSummaryModelDao.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/model/TestBusinessBundleSummaryModelDao.java
new file mode 100644
index 0000000..ac6bb30
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/model/TestBusinessBundleSummaryModelDao.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.dao.model;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
+import com.ning.billing.osgi.bundles.analytics.AnalyticsTestSuiteNoDB;
+
+public class TestBusinessBundleSummaryModelDao extends AnalyticsTestSuiteNoDB {
+
+    @Test(groups = "fast")
+    public void testConstructor() throws Exception {
+        final DateTime startDate = new DateTime(2012, 6, 5, 4, 3, 12, DateTimeZone.UTC);
+        final DateTime requestedTimestamp = new DateTime(2012, 7, 21, 10, 10, 10, DateTimeZone.UTC);
+
+        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.valueOf("ADD_BASE");
+        final BusinessSubscription previousSubscription = null;
+        final BusinessSubscription nextSubscription = new BusinessSubscription(null, null, null, Currency.GBP, startDate, SubscriptionState.ACTIVE);
+        final BusinessSubscriptionTransitionModelDao subscriptionTransitionModelDao = new BusinessSubscriptionTransitionModelDao(account,
+                                                                                                                                 accountRecordId,
+                                                                                                                                 bundle,
+                                                                                                                                 subscriptionTransition,
+                                                                                                                                 subscriptionEventRecordId,
+                                                                                                                                 requestedTimestamp,
+                                                                                                                                 event,
+                                                                                                                                 previousSubscription,
+                                                                                                                                 nextSubscription,
+                                                                                                                                 auditLog,
+                                                                                                                                 tenantRecordId,
+                                                                                                                                 reportGroup);
+
+        final BusinessBundleSummaryModelDao bundleSummaryModelDao = new BusinessBundleSummaryModelDao(account,
+                                                                                                      accountRecordId,
+                                                                                                      bundle,
+                                                                                                      bundleRecordId,
+                                                                                                      3,
+                                                                                                      subscriptionTransitionModelDao,
+                                                                                                      auditLog,
+                                                                                                      tenantRecordId,
+                                                                                                      reportGroup);
+        verifyBusinessModelDaoBase(bundleSummaryModelDao, accountRecordId, tenantRecordId);
+        Assert.assertEquals(bundleSummaryModelDao.getBundleRecordId(), bundleRecordId);
+        Assert.assertEquals(bundleSummaryModelDao.getBundleId(), bundle.getId());
+        Assert.assertEquals(bundleSummaryModelDao.getBundleExternalKey(), bundle.getExternalKey());
+        Assert.assertEquals(bundleSummaryModelDao.getSubscriptionId(), subscriptionTransition.getSubscriptionId());
+        Assert.assertEquals(bundleSummaryModelDao.getBundleAccountRank(), (Integer) 3);
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentProductName(), subscriptionTransitionModelDao.getNextProductName());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentProductType(), subscriptionTransitionModelDao.getNextProductType());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentProductCategory(), subscriptionTransitionModelDao.getNextProductCategory());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentSlug(), subscriptionTransitionModelDao.getNextSlug());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentPhase(), subscriptionTransitionModelDao.getNextPhase());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentBillingPeriod(), subscriptionTransitionModelDao.getNextBillingPeriod());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentPrice(), subscriptionTransitionModelDao.getNextPrice());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentPriceList(), subscriptionTransitionModelDao.getNextPriceList());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentMrr(), subscriptionTransitionModelDao.getNextMrr());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentCurrency(), subscriptionTransitionModelDao.getNextCurrency());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentBusinessActive(), subscriptionTransitionModelDao.getNextBusinessActive());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentStartDate(), subscriptionTransitionModelDao.getNextStartDate());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentEndDate(), subscriptionTransitionModelDao.getNextEndDate());
+        Assert.assertEquals(bundleSummaryModelDao.getCurrentState(), subscriptionTransitionModelDao.getNextState());
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/model/TestBusinessSubscriptionTransitionModelDao.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/model/TestBusinessSubscriptionTransitionModelDao.java
index 5041d64..3cacf4b 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/model/TestBusinessSubscriptionTransitionModelDao.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/model/TestBusinessSubscriptionTransitionModelDao.java
@@ -70,19 +70,19 @@ public class TestBusinessSubscriptionTransitionModelDao extends AnalyticsTestSui
         Assert.assertNull(subscriptionTransitionModelDao.getPrevStartDate());
         Assert.assertNull(subscriptionTransitionModelDao.getPrevState());
 
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextProductName(), subscriptionTransitionModelDao.getNextProductName());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextProductType(), subscriptionTransitionModelDao.getNextProductType());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextProductCategory(), subscriptionTransitionModelDao.getNextProductCategory());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextSlug(), subscriptionTransitionModelDao.getNextSlug());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextPhase(), subscriptionTransitionModelDao.getNextPhase());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextBillingPeriod(), subscriptionTransitionModelDao.getNextBillingPeriod());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextPrice(), subscriptionTransitionModelDao.getNextPrice());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextPriceList(), subscriptionTransitionModelDao.getNextPriceList());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextMrr(), subscriptionTransitionModelDao.getNextMrr());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextCurrency(), subscriptionTransitionModelDao.getNextCurrency());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextBusinessActive(), subscriptionTransitionModelDao.getNextBusinessActive());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextStartDate(), subscriptionTransitionModelDao.getNextStartDate());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextEndDate(), subscriptionTransitionModelDao.getNextEndDate());
-        Assert.assertEquals(subscriptionTransitionModelDao.getNextState(), subscriptionTransitionModelDao.getNextState());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextProductName(), nextSubscription.getProductName());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextProductType(), nextSubscription.getProductType());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextProductCategory(), nextSubscription.getProductCategory());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextSlug(), nextSubscription.getSlug());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextPhase(), nextSubscription.getPhase());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextBillingPeriod(), nextSubscription.getBillingPeriod());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextPrice(), nextSubscription.getPrice());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextPriceList(), nextSubscription.getPriceList());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextMrr(), nextSubscription.getMrr());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextCurrency(), nextSubscription.getCurrency());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextBusinessActive(), nextSubscription.getBusinessActive());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextStartDate(), nextSubscription.getStartDate());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextEndDate(), nextSubscription.getEndDate());
+        Assert.assertEquals(subscriptionTransitionModelDao.getNextState(), nextSubscription.getState());
     }
 }
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessAnalyticsSqlDao.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessAnalyticsSqlDao.java
index 00ecab6..2996eb4 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessAnalyticsSqlDao.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessAnalyticsSqlDao.java
@@ -29,6 +29,7 @@ import com.ning.billing.osgi.bundles.analytics.AnalyticsTestSuiteWithEmbeddedDB;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountFieldModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessAccountTagModelDao;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessBundleSummaryModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessFieldModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceFieldModelDao;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessInvoiceItemBaseModelDao;
@@ -281,6 +282,48 @@ public class TestBusinessAnalyticsSqlDao extends AnalyticsTestSuiteWithEmbeddedD
     }
 
     @Test(groups = "slow")
+    public void testSqlDaoForBundleSummary() throws Exception {
+        final DateTime startDate = new DateTime(2012, 6, 5, 4, 3, 12, DateTimeZone.UTC);
+        final DateTime requestedTimestamp = new DateTime(2012, 7, 21, 10, 10, 10, DateTimeZone.UTC);
+
+        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.valueOf("ADD_BASE");
+        final BusinessSubscription previousSubscription = null;
+        final BusinessSubscription nextSubscription = new BusinessSubscription(null, null, null, Currency.GBP, startDate, SubscriptionState.ACTIVE);
+        final BusinessSubscriptionTransitionModelDao businessSubscriptionTransitionModelDao = new BusinessSubscriptionTransitionModelDao(account,
+                                                                                                                                         accountRecordId,
+                                                                                                                                         bundle,
+                                                                                                                                         subscriptionTransition,
+                                                                                                                                         subscriptionEventRecordId,
+                                                                                                                                         requestedTimestamp,
+                                                                                                                                         event,
+                                                                                                                                         previousSubscription,
+                                                                                                                                         nextSubscription,
+                                                                                                                                         auditLog,
+                                                                                                                                         tenantRecordId,
+                                                                                                                                         reportGroup);
+        final BusinessBundleSummaryModelDao bundleSummaryModelDao = new BusinessBundleSummaryModelDao(account,
+                                                                                                      accountRecordId,
+                                                                                                      bundle,
+                                                                                                      bundleRecordId,
+                                                                                                      3,
+                                                                                                      businessSubscriptionTransitionModelDao,
+                                                                                                      auditLog,
+                                                                                                      tenantRecordId,
+                                                                                                      reportGroup);
+        // Check the record doesn't exist yet
+        Assert.assertEquals(analyticsSqlDao.getBundleSummariesByAccountRecordId(accountRecordId, tenantRecordId, callContext).size(), 0);
+
+        // Create and check we can retrieve it
+        analyticsSqlDao.create(bundleSummaryModelDao.getTableName(), bundleSummaryModelDao, callContext);
+        Assert.assertEquals(analyticsSqlDao.getBundleSummariesByAccountRecordId(accountRecordId, tenantRecordId, callContext).size(), 1);
+        Assert.assertEquals(analyticsSqlDao.getBundleSummariesByAccountRecordId(accountRecordId, tenantRecordId, callContext).get(0), bundleSummaryModelDao);
+
+        // Delete and verify it doesn't exist anymore
+        analyticsSqlDao.deleteByAccountRecordId(bundleSummaryModelDao.getTableName(), accountRecordId, tenantRecordId, callContext);
+        Assert.assertEquals(analyticsSqlDao.getBundleSummariesByAccountRecordId(accountRecordId, tenantRecordId, callContext).size(), 0);
+    }
+
+    @Test(groups = "slow")
     public void testSqlDaoForAccountTag() throws Exception {
         final BusinessTagModelDao businessTagModelDao = new BusinessAccountTagModelDao(account,
                                                                                        accountRecordId,
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessBundleSummaryDao.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessBundleSummaryDao.java
new file mode 100644
index 0000000..27e3bb2
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/dao/TestBusinessBundleSummaryDao.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.dao;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.sql.DataSource;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.Product;
+import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.osgi.bundles.analytics.AnalyticsTestSuiteNoDB;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscription;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscriptionEvent;
+import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscriptionTransitionModelDao;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillDataSource;
+import com.ning.killbill.osgi.libs.killbill.OSGIKillbillLogService;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestBusinessBundleSummaryDao extends AnalyticsTestSuiteNoDB {
+
+    private BusinessBundleSummaryDao bundleSummaryDao;
+
+    @Override
+    @BeforeMethod(groups = "fast")
+    public void setUp() throws Exception {
+        super.setUp();
+
+        final OSGIKillbillDataSource osgiKillbillDataSource = Mockito.mock(OSGIKillbillDataSource.class);
+
+        final DataSource dataSource = Mockito.mock(DataSource.class);
+        Mockito.when(osgiKillbillDataSource.getDataSource()).thenReturn(dataSource);
+
+        final OSGIKillbillLogService osgiKillbillLogService = Mockito.mock(OSGIKillbillLogService.class);
+        Mockito.doAnswer(new Answer() {
+            @Override
+            public Object answer(final InvocationOnMock invocation) throws Throwable {
+                logger.info(Arrays.toString(invocation.getArguments()));
+                return null;
+            }
+        }).when(osgiKillbillLogService).log(Mockito.anyInt(), Mockito.anyString());
+
+        bundleSummaryDao = new BusinessBundleSummaryDao(osgiKillbillLogService, null, osgiKillbillDataSource);
+    }
+
+    @Test(groups = "fast")
+    public void testFilterBsts() throws Exception {
+        final UUID bundleId1 = UUID.randomUUID();
+        final DateTime bundle1StartDate = new DateTime(2012, 1, 1, 1, 1);
+        final DateTime bundle1PhaseDate = new DateTime(2012, 2, 2, 1, 1);
+        final UUID bundleId2 = UUID.randomUUID();
+        final DateTime bundle2StartDate = new DateTime(2012, 2, 1, 1, 1);
+        final DateTime bundle2PhaseDate = new DateTime(2012, 3, 2, 1, 1);
+        final UUID bundleId3 = UUID.randomUUID();
+        final DateTime bundle3StartDate = new DateTime(2012, 3, 1, 1, 1);
+
+        // Real order is: bundleId1 ADD_BASE, bundleId2 ADD_BASE, bundleId1 SYSTEM_CHANGE_BASE, bundleId3 ADD_BASE bundleId2 SYSTEM_CHANGE_BASE
+        final Collection<BusinessSubscriptionTransitionModelDao> bsts = ImmutableList.<BusinessSubscriptionTransitionModelDao>of(
+                createBst(bundleId1, "ADD_BASE", bundle1StartDate),
+                createBst(bundleId1, "SYSTEM_CHANGE_BASE", bundle1PhaseDate),
+                createBst(bundleId2, "ADD_BASE", bundle2StartDate),
+                createBst(bundleId2, "SYSTEM_CHANGE_BASE", bundle2PhaseDate),
+                createBst(bundleId3, "ADD_BASE", bundle3StartDate),
+                createBst(UUID.randomUUID(), "ADD_ADD_ON", new DateTime(DateTimeZone.UTC))
+                                                                                                                                );
+
+        final Map<UUID, Integer> rankForBundle = new LinkedHashMap<UUID, Integer>();
+        final Map<UUID, BusinessSubscriptionTransitionModelDao> bstForBundle = new LinkedHashMap<UUID, BusinessSubscriptionTransitionModelDao>();
+        bundleSummaryDao.filterBstsForBasePlans(bsts, rankForBundle, bstForBundle);
+
+        final List<BusinessSubscriptionTransitionModelDao> filteredBsts = ImmutableList.<BusinessSubscriptionTransitionModelDao>copyOf(bstForBundle.values());
+        Assert.assertEquals(filteredBsts.size(), 3);
+
+        Assert.assertEquals(filteredBsts.get(0).getBundleId(), bundleId1);
+        Assert.assertEquals(filteredBsts.get(0).getNextStartDate(), bundle1PhaseDate);
+        Assert.assertEquals(filteredBsts.get(1).getBundleId(), bundleId2);
+        Assert.assertEquals(filteredBsts.get(1).getNextStartDate(), bundle2PhaseDate);
+        Assert.assertEquals(filteredBsts.get(2).getBundleId(), bundleId3);
+        Assert.assertEquals(filteredBsts.get(2).getNextStartDate(), bundle3StartDate);
+    }
+
+    private BusinessSubscriptionTransitionModelDao createBst(final UUID bundleId, final String eventString, final DateTime startDate) {
+        final SubscriptionBundle bundle = Mockito.mock(SubscriptionBundle.class);
+        Mockito.when(bundle.getId()).thenReturn(bundleId);
+
+        final DateTime requestedTimestamp = new DateTime(2012, 7, 21, 10, 10, 10, DateTimeZone.UTC);
+        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.valueOf(eventString);
+        final BusinessSubscription previousSubscription = null; // We don't look at it
+
+        final Product product = Mockito.mock(Product.class);
+        Mockito.when(product.getCategory()).thenReturn(event.getCategory());
+        final Plan plan = Mockito.mock(Plan.class);
+        Mockito.when(plan.getProduct()).thenReturn(product);
+        final BusinessSubscription nextSubscription = new BusinessSubscription(plan,
+                                                                               null,
+                                                                               null,
+                                                                               Currency.GBP,
+                                                                               startDate,
+                                                                               SubscriptionState.ACTIVE);
+
+        return new BusinessSubscriptionTransitionModelDao(account,
+                                                          accountRecordId,
+                                                          bundle,
+                                                          subscriptionTransition,
+                                                          subscriptionEventRecordId,
+                                                          requestedTimestamp,
+                                                          event,
+                                                          previousSubscription,
+                                                          nextSubscription,
+                                                          auditLog,
+                                                          tenantRecordId,
+                                                          reportGroup);
+    }
+}