killbill-uncached

Merge branch 'integration' of github.com:ning/killbill

6/23/2012 12:42:48 AM

Changes

analytics/src/test/java/com/ning/billing/analytics/dao/MockBusinessAccountSqlDao.java 44(+0 -44)

beatrix/pom.xml 5(+5 -0)

Details

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 caefaef..eb41bce 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
@@ -21,20 +21,37 @@ import com.google.inject.Inject;
 import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.account.api.AccountChangeEvent;
 import com.ning.billing.account.api.AccountCreationEvent;
+import com.ning.billing.entitlement.api.timeline.RepairEntitlementEvent;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 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;
 import com.ning.billing.payment.api.PaymentInfoEvent;
+import com.ning.billing.util.tag.api.ControlTagCreationEvent;
+import com.ning.billing.util.tag.api.ControlTagDefinitionCreationEvent;
+import com.ning.billing.util.tag.api.ControlTagDefinitionDeletionEvent;
+import com.ning.billing.util.tag.api.ControlTagDeletionEvent;
+import com.ning.billing.util.tag.api.UserTagCreationEvent;
+import com.ning.billing.util.tag.api.UserTagDefinitionCreationEvent;
+import com.ning.billing.util.tag.api.UserTagDefinitionDeletionEvent;
+import com.ning.billing.util.tag.api.UserTagDeletionEvent;
 
 public class AnalyticsListener {
     private final BusinessSubscriptionTransitionRecorder bstRecorder;
     private final BusinessAccountRecorder bacRecorder;
+    private final BusinessInvoiceRecorder invoiceRecorder;
+    private final BusinessTagRecorder tagRecorder;
 
     @Inject
-    public AnalyticsListener(final BusinessSubscriptionTransitionRecorder bstRecorder, final BusinessAccountRecorder bacRecorder) {
+    public AnalyticsListener(final BusinessSubscriptionTransitionRecorder bstRecorder,
+                             final BusinessAccountRecorder bacRecorder,
+                             final BusinessInvoiceRecorder invoiceRecorder,
+                             final BusinessTagRecorder tagRecorder) {
         this.bstRecorder = bstRecorder;
         this.bacRecorder = bacRecorder;
+        this.invoiceRecorder = invoiceRecorder;
+        this.tagRecorder = tagRecorder;
     }
 
     @Subscribe
@@ -77,12 +94,17 @@ public class AnalyticsListener {
             return;
         }
 
-        bacRecorder.accountUpdated(event.getAccountId(), event.getChangedFields());
+        bacRecorder.accountUpdated(event.getAccountId());
     }
 
     @Subscribe
-    public void handleInvoice(final InvoiceCreationEvent event) {
-        bacRecorder.accountUpdated(event.getAccountId());
+    public void handleInvoiceCreation(final InvoiceCreationEvent event) {
+        invoiceRecorder.invoiceCreated(event.getInvoiceId());
+    }
+
+    @Subscribe
+    public void handleNullInvoice(final EmptyInvoiceEvent event) {
+        // Ignored for now
     }
 
     @Subscribe
@@ -94,4 +116,49 @@ public class AnalyticsListener {
     public void handlePaymentError(final PaymentErrorEvent paymentError) {
         // TODO - we can't tie the error back to an account yet
     }
+
+    @Subscribe
+    public void handleControlTagCreation(final ControlTagCreationEvent event) {
+        tagRecorder.tagAdded(event.getObjectType(), event.getObjectId(), event.getTagDefinition().getName());
+    }
+
+    @Subscribe
+    public void handleControlTagDeletion(final ControlTagDeletionEvent event) {
+        tagRecorder.tagRemoved(event.getObjectType(), event.getObjectId(), event.getTagDefinition().getName());
+    }
+
+    @Subscribe
+    public void handleUserTagCreation(final UserTagCreationEvent event) {
+        tagRecorder.tagAdded(event.getObjectType(), event.getObjectId(), event.getTagDefinition().getName());
+    }
+
+    @Subscribe
+    public void handleUserTagDeletion(final UserTagDeletionEvent event) {
+        tagRecorder.tagRemoved(event.getObjectType(), event.getObjectId(), event.getTagDefinition().getName());
+    }
+
+    @Subscribe
+    public void handleControlTagDefinitionCreation(final ControlTagDefinitionCreationEvent event) {
+        // Ignored for now
+    }
+
+    @Subscribe
+    public void handleControlTagDefinitionDeletion(final ControlTagDefinitionDeletionEvent event) {
+        // Ignored for now
+    }
+
+    @Subscribe
+    public void handleUserTagDefinitionCreation(final UserTagDefinitionCreationEvent event) {
+        // Ignored for now
+    }
+
+    @Subscribe
+    public void handleUserTagDefinitionDeletion(final UserTagDefinitionDeletionEvent event) {
+        // Ignored for now
+    }
+
+    @Subscribe
+    public void handleRepairEntitlement(final RepairEntitlementEvent event) {
+        // Ignored for now
+    }
 }
diff --git a/analytics/src/main/java/com/ning/billing/analytics/api/user/DefaultAnalyticsUserApi.java b/analytics/src/main/java/com/ning/billing/analytics/api/user/DefaultAnalyticsUserApi.java
new file mode 100644
index 0000000..424e1c2
--- /dev/null
+++ b/analytics/src/main/java/com/ning/billing/analytics/api/user/DefaultAnalyticsUserApi.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2012 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.analytics.api.user;
+
+import javax.inject.Inject;
+import java.util.List;
+import java.util.UUID;
+
+import com.ning.billing.analytics.dao.AnalyticsDao;
+import com.ning.billing.analytics.model.BusinessAccount;
+import com.ning.billing.analytics.model.BusinessAccountTag;
+import com.ning.billing.analytics.model.BusinessInvoice;
+import com.ning.billing.analytics.model.BusinessInvoiceItem;
+import com.ning.billing.analytics.model.BusinessSubscriptionTransition;
+
+// Note: not exposed in api yet
+public class DefaultAnalyticsUserApi {
+    private final AnalyticsDao analyticsDao;
+
+    @Inject
+    public DefaultAnalyticsUserApi(final AnalyticsDao analyticsDao) {
+        this.analyticsDao = analyticsDao;
+    }
+
+    public BusinessAccount getAccountByKey(final String accountKey) {
+        return analyticsDao.getAccountByKey(accountKey);
+    }
+
+    public List<BusinessSubscriptionTransition> getTransitionsForBundle(final String externalKey) {
+        return analyticsDao.getTransitionsByKey(externalKey);
+    }
+
+    public List<BusinessInvoice> getInvoicesForAccount(final String accountKey) {
+        return analyticsDao.getInvoicesByKey(accountKey);
+    }
+
+    public List<BusinessAccountTag> getTagsForAccount(final String accountKey) {
+        return analyticsDao.getTagsForAccount(accountKey);
+    }
+
+    public List<BusinessInvoiceItem> getInvoiceItemsForInvoice(final UUID invoiceId) {
+        return analyticsDao.getInvoiceItemsForInvoice(invoiceId.toString());
+    }
+}
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessAccountRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessAccountRecorder.java
index 7f69fef..998fd7e 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/BusinessAccountRecorder.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessAccountRecorder.java
@@ -17,9 +17,7 @@
 package com.ning.billing.analytics;
 
 import java.math.BigDecimal;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -31,7 +29,6 @@ import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.account.api.AccountData;
 import com.ning.billing.account.api.AccountUserApi;
-import com.ning.billing.account.api.ChangedField;
 import com.ning.billing.analytics.dao.BusinessAccountSqlDao;
 import com.ning.billing.analytics.model.BusinessAccount;
 import com.ning.billing.invoice.api.Invoice;
@@ -40,9 +37,6 @@ import com.ning.billing.payment.api.Payment;
 import com.ning.billing.payment.api.PaymentApi;
 import com.ning.billing.payment.api.PaymentApiException;
 import com.ning.billing.payment.api.PaymentInfoEvent;
-import com.ning.billing.util.api.TagUserApi;
-import com.ning.billing.util.dao.ObjectType;
-import com.ning.billing.util.tag.Tag;
 
 public class BusinessAccountRecorder {
     private static final Logger log = LoggerFactory.getLogger(BusinessAccountRecorder.class);
@@ -51,25 +45,22 @@ public class BusinessAccountRecorder {
     private final AccountUserApi accountApi;
     private final InvoiceUserApi invoiceUserApi;
     private final PaymentApi paymentApi;
-    private final TagUserApi tagUserApi;
 
     @Inject
     public BusinessAccountRecorder(final BusinessAccountSqlDao sqlDao, final AccountUserApi accountApi,
-                                   final InvoiceUserApi invoiceUserApi, final PaymentApi paymentApi,
-                                   final TagUserApi tagUserApi) {
+                                   final InvoiceUserApi invoiceUserApi, final PaymentApi paymentApi) {
         this.sqlDao = sqlDao;
         this.accountApi = accountApi;
         this.invoiceUserApi = invoiceUserApi;
         this.paymentApi = paymentApi;
-        this.tagUserApi = tagUserApi;
     }
 
     public void accountCreated(final AccountData data) {
         final Account account;
         try {
             account = accountApi.getAccountByKey(data.getExternalKey());
-            final Map<String, Tag> tags = tagUserApi.getTags(account.getId(), ObjectType.ACCOUNT);
-            final BusinessAccount bac = createBusinessAccountFromAccount(account, new ArrayList<Tag>(tags.values()));
+            final BusinessAccount bac = new BusinessAccount();
+            updateBusinessAccountFromAccount(account, bac);
 
             log.info("ACCOUNT CREATION " + bac);
             sqlDao.createAccount(bac);
@@ -79,17 +70,6 @@ public class BusinessAccountRecorder {
     }
 
     /**
-     * Notification handler for Account changes
-     *
-     * @param accountId     account id changed
-     * @param changedFields list of changed fields
-     */
-    public void accountUpdated(final UUID accountId, final List<ChangedField> changedFields) {
-        // None of the fields updated interest us so far - see DefaultAccountChangeNotification
-        // TODO We'll need notifications for tags changes eventually
-    }
-
-    /**
      * Notification handler for Payment creations
      *
      * @param paymentInfo payment object (from the payment plugin)
@@ -111,16 +91,11 @@ public class BusinessAccountRecorder {
     public void accountUpdated(final UUID accountId) {
         try {
             final Account account = accountApi.getAccountById(accountId);
-            final Map<String, Tag> tags = tagUserApi.getTags(accountId, ObjectType.ACCOUNT);
-
-            if (account == null) {
-                log.warn("Couldn't find account {}", accountId);
-                return;
-            }
 
             BusinessAccount bac = sqlDao.getAccount(account.getExternalKey());
             if (bac == null) {
-                bac = createBusinessAccountFromAccount(account, new ArrayList<Tag>(tags.values()));
+                bac = new BusinessAccount();
+                updateBusinessAccountFromAccount(account, bac);
                 log.info("ACCOUNT CREATION " + bac);
                 sqlDao.createAccount(bac);
             } else {
@@ -131,44 +106,25 @@ public class BusinessAccountRecorder {
         } catch (AccountApiException e) {
             log.warn("Error encountered creating BusinessAccount", e);
         }
-
-    }
-
-    private BusinessAccount createBusinessAccountFromAccount(final Account account, final List<Tag> tags) {
-        final BusinessAccount bac = new BusinessAccount(
-                account.getExternalKey(),
-                account.getName(),
-                invoiceUserApi.getAccountBalance(account.getId()),
-                // These fields will be updated below
-                null,
-                null,
-                null,
-                null,
-                null,
-                null
-        );
-        updateBusinessAccountFromAccount(account, bac);
-
-        return bac;
     }
 
     private void updateBusinessAccountFromAccount(final Account account, final BusinessAccount bac) {
+        bac.setName(account.getName());
+        bac.setKey(account.getExternalKey());
 
-        final List<UUID> invoiceIds = new ArrayList<UUID>();
         try {
-            DateTime lastInvoiceDate = null;
-            BigDecimal totalInvoiceBalance = BigDecimal.ZERO;
-            String lastPaymentStatus = null;
-            String paymentMethod = null;
-            String creditCardType = null;
-            String billingAddressCountry = null;
+            DateTime lastInvoiceDate = bac.getLastInvoiceDate();
+            BigDecimal totalInvoiceBalance = bac.getTotalInvoiceBalance();
+            String lastPaymentStatus = bac.getLastPaymentStatus();
+            String paymentMethod = bac.getPaymentMethod();
+            String creditCardType = bac.getCreditCardType();
+            String billingAddressCountry = bac.getBillingAddressCountry();
 
             // Retrieve invoices information
             final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId());
             if (invoices != null && invoices.size() > 0) {
 
                 for (final Invoice invoice : invoices) {
-                    invoiceIds.add(invoice.getId());
                     totalInvoiceBalance = totalInvoiceBalance.add(invoice.getBalance());
 
                     if (lastInvoiceDate == null || invoice.getInvoiceDate().isAfter(lastInvoiceDate)) {
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessInvoiceRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessInvoiceRecorder.java
new file mode 100644
index 0000000..21693f3
--- /dev/null
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessInvoiceRecorder.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2010-2012 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.analytics;
+
+import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.analytics.dao.AnalyticsDao;
+import com.ning.billing.analytics.model.BusinessInvoice;
+import com.ning.billing.analytics.model.BusinessInvoiceItem;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+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.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.api.InvoiceUserApi;
+
+public class BusinessInvoiceRecorder {
+    private static final Logger log = LoggerFactory.getLogger(BusinessInvoiceRecorder.class);
+
+    private final AnalyticsDao analyticsDao;
+    private final AccountUserApi accountApi;
+    private final EntitlementUserApi entitlementApi;
+    private final InvoiceUserApi invoiceApi;
+
+    @Inject
+    public BusinessInvoiceRecorder(final AnalyticsDao analyticsDao,
+                                   final AccountUserApi accountApi,
+                                   final EntitlementUserApi entitlementApi,
+                                   final InvoiceUserApi invoiceApi) {
+        this.analyticsDao = analyticsDao;
+        this.accountApi = accountApi;
+        this.entitlementApi = entitlementApi;
+        this.invoiceApi = invoiceApi;
+    }
+
+    public void invoiceCreated(final UUID invoiceId) {
+        // Lookup the invoice object
+        final Invoice invoice = invoiceApi.getInvoice(invoiceId);
+        if (invoice == null) {
+            log.warn("Ignoring invoice creation for invoice id {} (invoice does not exist)", invoiceId.toString());
+            return;
+        }
+
+        // Lookup the associated account
+        final String accountKey;
+        try {
+            final Account account = accountApi.getAccountById(invoice.getAccountId());
+            accountKey = account.getExternalKey();
+        } catch (AccountApiException e) {
+            log.warn("Ignoring invoice creation for invoice id {} and account id {} (account does not exist)",
+                     invoice.getId().toString(),
+                     invoice.getAccountId().toString());
+            return;
+        }
+
+        // Create the invoice
+        final BusinessInvoice businessInvoice = new BusinessInvoice(accountKey, invoice);
+
+        // Create associated invoice items
+        final List<BusinessInvoiceItem> businessInvoiceItems = new ArrayList<BusinessInvoiceItem>();
+        for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+            final BusinessInvoiceItem businessInvoiceItem = createBusinessInvoiceItem(invoiceItem);
+            if (businessInvoiceItem != null) {
+                businessInvoiceItems.add(businessInvoiceItem);
+            }
+        }
+
+        // Update the Analytics tables
+        analyticsDao.createInvoice(accountKey, businessInvoice, businessInvoiceItems);
+    }
+
+    private BusinessInvoiceItem createBusinessInvoiceItem(final InvoiceItem invoiceItem) {
+        final String externalKey;
+        try {
+            final SubscriptionBundle bundle = entitlementApi.getBundleFromId(invoiceItem.getBundleId());
+            externalKey = bundle.getKey();
+        } catch (EntitlementUserApiException e) {
+            log.warn("Ignoring invoice item {} for bundle {} (bundle does not exist)",
+                     invoiceItem.getId().toString(),
+                     invoiceItem.getBundleId().toString());
+            return null;
+        }
+
+        final Subscription subscription;
+        try {
+            subscription = entitlementApi.getSubscriptionFromId(invoiceItem.getSubscriptionId());
+        } catch (EntitlementUserApiException e) {
+            log.warn("Ignoring invoice item {} for subscription {} (subscription does not exist)",
+                     invoiceItem.getId().toString(),
+                     invoiceItem.getSubscriptionId().toString());
+            return null;
+        }
+
+        final Plan plan = subscription.getCurrentPlan();
+        if (plan == null) {
+            log.warn("Ignoring invoice item {} for subscription {} (null plan)",
+                     invoiceItem.getId().toString(),
+                     invoiceItem.getSubscriptionId().toString());
+            return null;
+        }
+
+        final PlanPhase planPhase = subscription.getCurrentPhase();
+        if (planPhase == null) {
+            log.warn("Ignoring invoice item {} for subscription {} (null phase)",
+                     invoiceItem.getId().toString(),
+                     invoiceItem.getSubscriptionId().toString());
+            return null;
+        }
+
+        return new BusinessInvoiceItem(externalKey, invoiceItem, plan, planPhase);
+    }
+}
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessTagRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessTagRecorder.java
new file mode 100644
index 0000000..8aa4343
--- /dev/null
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessTagRecorder.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2010-2012 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.analytics;
+
+import javax.inject.Inject;
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.analytics.dao.BusinessAccountTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoicePaymentTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoiceTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessSubscriptionTransitionTagSqlDao;
+import com.ning.billing.entitlement.api.user.EntitlementUserApi;
+import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.util.dao.ObjectType;
+
+public class BusinessTagRecorder {
+    private static final Logger log = LoggerFactory.getLogger(BusinessTagRecorder.class);
+
+    private final BusinessAccountTagSqlDao accountTagSqlDao;
+    private final BusinessInvoiceTagSqlDao invoiceTagSqlDao;
+    private final BusinessInvoicePaymentTagSqlDao invoicePaymentTagSqlDao;
+    private final BusinessSubscriptionTransitionTagSqlDao subscriptionTransitionTagSqlDao;
+    private final AccountUserApi accountApi;
+    private final EntitlementUserApi entitlementUserApi;
+
+    @Inject
+    public BusinessTagRecorder(final BusinessAccountTagSqlDao accountTagSqlDao,
+                               final BusinessInvoicePaymentTagSqlDao invoicePaymentTagSqlDao,
+                               final BusinessInvoiceTagSqlDao invoiceTagSqlDao,
+                               final BusinessSubscriptionTransitionTagSqlDao subscriptionTransitionTagSqlDao,
+                               final AccountUserApi accountApi,
+                               final EntitlementUserApi entitlementUserApi) {
+        this.accountTagSqlDao = accountTagSqlDao;
+        this.invoicePaymentTagSqlDao = invoicePaymentTagSqlDao;
+        this.invoiceTagSqlDao = invoiceTagSqlDao;
+        this.subscriptionTransitionTagSqlDao = subscriptionTransitionTagSqlDao;
+        this.accountApi = accountApi;
+        this.entitlementUserApi = entitlementUserApi;
+    }
+
+    public void tagAdded(final ObjectType objectType, final UUID objectId, final String name) {
+        if (objectType.equals(ObjectType.ACCOUNT)) {
+            tagAddedForAccount(objectId, name);
+        } else if (objectType.equals(ObjectType.BUNDLE)) {
+            tagAddedForBundle(objectId, name);
+        } else if (objectType.equals(ObjectType.INVOICE)) {
+            tagAddedForInvoice(objectId, name);
+        } else if (objectType.equals(ObjectType.PAYMENT)) {
+            tagAddedForPayment(objectId, name);
+        } else {
+            log.info("Ignoring tag addition of {} for object id {} (type {})", new Object[]{name, objectId.toString(), objectType.toString()});
+        }
+    }
+
+    public void tagRemoved(final ObjectType objectType, final UUID objectId, final String name) {
+        if (objectType.equals(ObjectType.ACCOUNT)) {
+            tagRemovedForAccount(objectId, name);
+        } else if (objectType.equals(ObjectType.BUNDLE)) {
+            tagRemovedForBundle(objectId, name);
+        } else if (objectType.equals(ObjectType.INVOICE)) {
+            tagRemovedForInvoice(objectId, name);
+        } else if (objectType.equals(ObjectType.PAYMENT)) {
+            tagRemovedForPayment(objectId, name);
+        } else {
+            log.info("Ignoring tag removal of {} for object id {} (type {})", new Object[]{name, objectId.toString(), objectType.toString()});
+        }
+    }
+
+    private void tagAddedForAccount(final UUID objectId, final String name) {
+        final Account account;
+        try {
+            account = accountApi.getAccountById(objectId);
+        } catch (AccountApiException e) {
+            log.warn("Ignoring tag addition of {} for account id {} (account does not exist)", name, objectId.toString());
+            return;
+        }
+
+        final String accountKey = account.getExternalKey();
+        accountTagSqlDao.addTag(accountKey, name);
+    }
+
+    private void tagRemovedForAccount(final UUID objectId, final String name) {
+        final Account account;
+        try {
+            account = accountApi.getAccountById(objectId);
+        } catch (AccountApiException e) {
+            log.warn("Ignoring tag removal of {} for account id {} (account does not exist)", name, objectId.toString());
+            return;
+        }
+
+        final String accountKey = account.getExternalKey();
+        accountTagSqlDao.removeTag(accountKey, name);
+    }
+
+    private void tagAddedForBundle(final UUID objectId, final String name) {
+        final SubscriptionBundle bundle;
+        try {
+            bundle = entitlementUserApi.getBundleFromId(objectId);
+        } catch (EntitlementUserApiException e) {
+            log.warn("Ignoring tag addition of {} for bundle id {} (bundle does not exist)", name, objectId.toString());
+            return;
+        }
+
+        /*
+         * Note: we store tags associated to bundles, not to subscriptions.
+         * Subscriptions are in the core of killbill and not exposed in Analytics to avoid a hard dependency
+         * (i.e. dashboards should not rely on killbill ids).
+         */
+        final String externalKey = bundle.getKey();
+        subscriptionTransitionTagSqlDao.addTag(externalKey, name);
+    }
+
+    private void tagRemovedForBundle(final UUID objectId, final String name) {
+        final SubscriptionBundle bundle;
+        try {
+            bundle = entitlementUserApi.getBundleFromId(objectId);
+        } catch (EntitlementUserApiException e) {
+            log.warn("Ignoring tag removal of {} for bundle id {} (bundle does not exist)", name, objectId.toString());
+            return;
+        }
+
+        final String externalKey = bundle.getKey();
+        subscriptionTransitionTagSqlDao.removeTag(externalKey, name);
+    }
+
+    private void tagAddedForInvoice(final UUID objectId, final String name) {
+        invoiceTagSqlDao.addTag(objectId.toString(), name);
+    }
+
+    private void tagRemovedForInvoice(final UUID objectId, final String name) {
+        invoiceTagSqlDao.removeTag(objectId.toString(), name);
+    }
+
+    private void tagAddedForPayment(final UUID objectId, final String name) {
+        invoicePaymentTagSqlDao.addTag(objectId.toString(), name);
+    }
+
+    private void tagRemovedForPayment(final UUID objectId, final String name) {
+        invoicePaymentTagSqlDao.removeTag(objectId.toString(), name);
+    }
+}
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessAccountMapper.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessAccountMapper.java
index a28864b..bf4da3d 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessAccountMapper.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessAccountMapper.java
@@ -34,7 +34,7 @@ public class BusinessAccountMapper implements ResultSetMapper<BusinessAccount> {
                 r.getString(1),
                 r.getString(5),
                 BigDecimal.valueOf(r.getDouble(4)),
-                new DateTime(r.getLong(6), DateTimeZone.UTC),
+                r.getLong(6) == 0 ? null : new DateTime(r.getLong(6), DateTimeZone.UTC),
                 BigDecimal.valueOf(r.getDouble(7)),
                 r.getString(8),
                 r.getString(9),
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessAccountSqlDao.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessAccountSqlDao.java
index 09a5506..605e604 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessAccountSqlDao.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessAccountSqlDao.java
@@ -20,13 +20,15 @@ import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
 import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
 
 import com.ning.billing.analytics.model.BusinessAccount;
 
 @ExternalizedSqlViaStringTemplate3()
 @RegisterMapper(BusinessAccountMapper.class)
-public interface BusinessAccountSqlDao {
+public interface BusinessAccountSqlDao extends Transactional<BusinessAccountSqlDao>, Transmogrifier {
     @SqlQuery
     BusinessAccount getAccount(@Bind("account_key") final String key);
 
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessInvoiceItemSqlDao.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessInvoiceItemSqlDao.java
index 6f491d6..dd18e3a 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessInvoiceItemSqlDao.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessInvoiceItemSqlDao.java
@@ -22,13 +22,15 @@ import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
 import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
 
 import com.ning.billing.analytics.model.BusinessInvoiceItem;
 
 @ExternalizedSqlViaStringTemplate3()
 @RegisterMapper(BusinessInvoiceItemMapper.class)
-public interface BusinessInvoiceItemSqlDao {
+public interface BusinessInvoiceItemSqlDao extends Transactional<BusinessInvoiceItemSqlDao>, Transmogrifier {
     @SqlQuery
     BusinessInvoiceItem getInvoiceItem(@Bind("item_id") final String itemId);
 
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessInvoiceSqlDao.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessInvoiceSqlDao.java
index abfa302..d3c8c6e 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessInvoiceSqlDao.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessInvoiceSqlDao.java
@@ -22,13 +22,15 @@ import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
 import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
 
 import com.ning.billing.analytics.model.BusinessInvoice;
 
 @ExternalizedSqlViaStringTemplate3()
 @RegisterMapper(BusinessInvoiceMapper.class)
-public interface BusinessInvoiceSqlDao {
+public interface BusinessInvoiceSqlDao extends Transactional<BusinessInvoiceSqlDao>, Transmogrifier {
     @SqlQuery
     BusinessInvoice getInvoice(@Bind("invoice_id") final String invoiceId);
 
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/DefaultAnalyticsDao.java b/analytics/src/main/java/com/ning/billing/analytics/dao/DefaultAnalyticsDao.java
new file mode 100644
index 0000000..d5f8627
--- /dev/null
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/DefaultAnalyticsDao.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2012 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.analytics.dao;
+
+import javax.inject.Inject;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.skife.jdbi.v2.Transaction;
+import org.skife.jdbi.v2.TransactionStatus;
+
+import com.ning.billing.analytics.model.BusinessAccount;
+import com.ning.billing.analytics.model.BusinessAccountTag;
+import com.ning.billing.analytics.model.BusinessInvoice;
+import com.ning.billing.analytics.model.BusinessInvoiceItem;
+import com.ning.billing.analytics.model.BusinessSubscriptionTransition;
+
+public class DefaultAnalyticsDao implements AnalyticsDao {
+    private final BusinessAccountSqlDao accountSqlDao;
+    private final BusinessSubscriptionTransitionSqlDao subscriptionTransitionSqlDao;
+    private final BusinessInvoiceSqlDao invoiceSqlDao;
+    private final BusinessInvoiceItemSqlDao invoiceItemSqlDao;
+    private final BusinessAccountTagSqlDao accountTagSqlDao;
+
+    @Inject
+    public DefaultAnalyticsDao(final BusinessAccountSqlDao accountSqlDao,
+                               final BusinessSubscriptionTransitionSqlDao subscriptionTransitionSqlDao,
+                               final BusinessInvoiceSqlDao invoiceSqlDao,
+                               final BusinessInvoiceItemSqlDao invoiceItemSqlDao,
+                               final BusinessAccountTagSqlDao accountTagSqlDao) {
+        this.accountSqlDao = accountSqlDao;
+        this.subscriptionTransitionSqlDao = subscriptionTransitionSqlDao;
+        this.invoiceSqlDao = invoiceSqlDao;
+        this.invoiceItemSqlDao = invoiceItemSqlDao;
+        this.accountTagSqlDao = accountTagSqlDao;
+    }
+
+    @Override
+    public BusinessAccount getAccountByKey(final String accountKey) {
+        return accountSqlDao.getAccount(accountKey);
+    }
+
+    @Override
+    public List<BusinessSubscriptionTransition> getTransitionsByKey(final String externalKey) {
+        return subscriptionTransitionSqlDao.getTransitions(externalKey);
+    }
+
+    @Override
+    public List<BusinessInvoice> getInvoicesByKey(final String accountKey) {
+        return invoiceSqlDao.getInvoicesForAccount(accountKey);
+    }
+
+    @Override
+    public List<BusinessAccountTag> getTagsForAccount(final String accountKey) {
+        return accountTagSqlDao.getTagsForAccount(accountKey);
+    }
+
+    @Override
+    public List<BusinessInvoiceItem> getInvoiceItemsForInvoice(final String invoiceId) {
+        return invoiceItemSqlDao.getInvoiceItemsForInvoice(invoiceId);
+    }
+
+    @Override
+    public void createInvoice(final String accountKey, final BusinessInvoice invoice, final Iterable<BusinessInvoiceItem> invoiceItems) {
+        invoiceSqlDao.inTransaction(new Transaction<Void, BusinessInvoiceSqlDao>() {
+            @Override
+            public Void inTransaction(final BusinessInvoiceSqlDao transactional, final TransactionStatus status) throws Exception {
+                // Create the invoice
+                transactional.createInvoice(invoice);
+
+                // Add associated invoice items
+                final BusinessInvoiceItemSqlDao invoiceItemSqlDao = transactional.become(BusinessInvoiceItemSqlDao.class);
+                for (final BusinessInvoiceItem invoiceItem : invoiceItems) {
+                    invoiceItemSqlDao.createInvoiceItem(invoiceItem);
+                }
+
+                // Update BAC
+                final BusinessAccountSqlDao accountSqlDao = transactional.become(BusinessAccountSqlDao.class);
+                final BusinessAccount account = accountSqlDao.getAccount(accountKey);
+                if (account == null) {
+                    throw new IllegalStateException("Account does not exist for key " + accountKey);
+                }
+                account.setBalance(account.getBalance().add(invoice.getBalance()));
+                account.setLastInvoiceDate(invoice.getInvoiceDate());
+                account.setTotalInvoiceBalance(account.getTotalInvoiceBalance().add(invoice.getBalance()));
+                account.setUpdatedDt(new DateTime(DateTimeZone.UTC));
+                accountSqlDao.saveAccount(account);
+
+                return null;
+            }
+        });
+    }
+}
diff --git a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessAccount.java b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessAccount.java
index a74fea3..b8b056c 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessAccount.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessAccount.java
@@ -27,7 +27,7 @@ public class BusinessAccount {
     private DateTime createdDt = null;
     private DateTime updatedDt = null;
 
-    private final String key;
+    private String key;
     private String name;
     private BigDecimal balance;
     private DateTime lastInvoiceDate;
@@ -37,6 +37,9 @@ public class BusinessAccount {
     private String creditCardType;
     private String billingAddressCountry;
 
+    public BusinessAccount() {
+    }
+
     public BusinessAccount(final String key, final String name, final BigDecimal balance, final DateTime lastInvoiceDate, final BigDecimal totalInvoiceBalance, final String lastPaymentStatus, final String paymentMethod, final String creditCardType, final String billingAddressCountry) {
         this.key = key;
         this.balance = balance;
@@ -53,6 +56,10 @@ public class BusinessAccount {
         return key;
     }
 
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
     public BigDecimal getBalance() {
         return balance;
     }
diff --git a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessInvoice.java b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessInvoice.java
index a52383f..31591c6 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessInvoice.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessInvoice.java
@@ -20,9 +20,11 @@ import java.math.BigDecimal;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 
 import com.ning.billing.analytics.utils.Rounder;
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
 
 public class BusinessInvoice {
     private final UUID invoiceId;
@@ -55,6 +57,12 @@ public class BusinessInvoice {
         this.updatedDate = updatedDate;
     }
 
+    public BusinessInvoice(final String accountKey, final Invoice invoice) {
+        this(accountKey, invoice.getAmountCharged(), invoice.getAmountCredited(), invoice.getAmountPaid(), invoice.getBalance(),
+             new DateTime(DateTimeZone.UTC), invoice.getCurrency(), invoice.getInvoiceDate(), invoice.getId(), invoice.getTargetDate(),
+             new DateTime(DateTimeZone.UTC));
+    }
+
     public DateTime getCreatedDate() {
         return createdDate;
     }
diff --git a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessInvoiceItem.java b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessInvoiceItem.java
index 6538712..03cb50c 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessInvoiceItem.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessInvoiceItem.java
@@ -20,9 +20,13 @@ import java.math.BigDecimal;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 
 import com.ning.billing.analytics.utils.Rounder;
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.invoice.api.InvoiceItem;
 
 public class BusinessInvoiceItem {
     private final UUID itemId;
@@ -66,6 +70,13 @@ public class BusinessInvoiceItem {
         this.updatedDate = updatedDate;
     }
 
+    public BusinessInvoiceItem(final String externalKey, final InvoiceItem invoiceItem, final Plan plan, final PlanPhase planPhase) {
+        this(invoiceItem.getAmount(), planPhase.getBillingPeriod().toString(), new DateTime(DateTimeZone.UTC), invoiceItem.getCurrency(), invoiceItem.getEndDate(),
+             externalKey, invoiceItem.getInvoiceId(), invoiceItem.getId(), invoiceItem.getInvoiceItemType().toString(),
+             planPhase.getPhaseType().toString(), plan.getProduct().getCategory().toString(), plan.getProduct().getName(), plan.getProduct().getCatalogName(),
+             planPhase.getName(), invoiceItem.getStartDate(), new DateTime(DateTimeZone.UTC));
+    }
+
     public DateTime getCreatedDate() {
         return createdDate;
     }
diff --git a/analytics/src/main/java/com/ning/billing/analytics/setup/AnalyticsModule.java b/analytics/src/main/java/com/ning/billing/analytics/setup/AnalyticsModule.java
index bb564e6..82433ec 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/setup/AnalyticsModule.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/setup/AnalyticsModule.java
@@ -21,23 +21,52 @@ import com.google.inject.AbstractModule;
 import com.ning.billing.analytics.AnalyticsListener;
 import com.ning.billing.analytics.BusinessAccountRecorder;
 import com.ning.billing.analytics.BusinessSubscriptionTransitionRecorder;
+import com.ning.billing.analytics.BusinessTagRecorder;
 import com.ning.billing.analytics.api.AnalyticsService;
 import com.ning.billing.analytics.api.DefaultAnalyticsService;
+import com.ning.billing.analytics.api.user.DefaultAnalyticsUserApi;
+import com.ning.billing.analytics.dao.AnalyticsDao;
 import com.ning.billing.analytics.dao.BusinessAccountSqlDao;
-import com.ning.billing.analytics.dao.BusinessAccountSqlDaoProvider;
+import com.ning.billing.analytics.dao.BusinessAccountTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoiceFieldSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoiceItemSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoicePaymentFieldSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoicePaymentSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoicePaymentTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoiceSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoiceTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessOverdueStatusSqlDao;
+import com.ning.billing.analytics.dao.BusinessSqlProvider;
+import com.ning.billing.analytics.dao.BusinessSubscriptionTransitionFieldSqlDao;
 import com.ning.billing.analytics.dao.BusinessSubscriptionTransitionSqlDao;
-import com.ning.billing.analytics.dao.BusinessSubscriptionTransitionSqlDaoProvider;
+import com.ning.billing.analytics.dao.BusinessSubscriptionTransitionTagSqlDao;
+import com.ning.billing.analytics.dao.DefaultAnalyticsDao;
 
 public class AnalyticsModule extends AbstractModule {
     @Override
     protected void configure() {
-        bind(BusinessSubscriptionTransitionSqlDao.class).toProvider(BusinessSubscriptionTransitionSqlDaoProvider.class).asEagerSingleton();
-        bind(BusinessAccountSqlDao.class).toProvider(BusinessAccountSqlDaoProvider.class).asEagerSingleton();
+        bind(BusinessAccountSqlDao.class).toProvider(new BusinessSqlProvider<BusinessAccountSqlDao>(BusinessAccountSqlDao.class));
+        bind(BusinessAccountTagSqlDao.class).toProvider(new BusinessSqlProvider<BusinessAccountTagSqlDao>(BusinessAccountTagSqlDao.class));
+        bind(BusinessInvoiceFieldSqlDao.class).toProvider(new BusinessSqlProvider<BusinessInvoiceFieldSqlDao>(BusinessInvoiceFieldSqlDao.class));
+        bind(BusinessInvoiceItemSqlDao.class).toProvider(new BusinessSqlProvider<BusinessInvoiceItemSqlDao>(BusinessInvoiceItemSqlDao.class));
+        bind(BusinessInvoicePaymentFieldSqlDao.class).toProvider(new BusinessSqlProvider<BusinessInvoicePaymentFieldSqlDao>(BusinessInvoicePaymentFieldSqlDao.class));
+        bind(BusinessInvoicePaymentSqlDao.class).toProvider(new BusinessSqlProvider<BusinessInvoicePaymentSqlDao>(BusinessInvoicePaymentSqlDao.class));
+        bind(BusinessInvoicePaymentTagSqlDao.class).toProvider(new BusinessSqlProvider<BusinessInvoicePaymentTagSqlDao>(BusinessInvoicePaymentTagSqlDao.class));
+        bind(BusinessInvoiceSqlDao.class).toProvider(new BusinessSqlProvider<BusinessInvoiceSqlDao>(BusinessInvoiceSqlDao.class));
+        bind(BusinessInvoiceTagSqlDao.class).toProvider(new BusinessSqlProvider<BusinessInvoiceTagSqlDao>(BusinessInvoiceTagSqlDao.class));
+        bind(BusinessOverdueStatusSqlDao.class).toProvider(new BusinessSqlProvider<BusinessOverdueStatusSqlDao>(BusinessOverdueStatusSqlDao.class));
+        bind(BusinessSubscriptionTransitionFieldSqlDao.class).toProvider(new BusinessSqlProvider<BusinessSubscriptionTransitionFieldSqlDao>(BusinessSubscriptionTransitionFieldSqlDao.class));
+        bind(BusinessSubscriptionTransitionSqlDao.class).toProvider(new BusinessSqlProvider<BusinessSubscriptionTransitionSqlDao>(BusinessSubscriptionTransitionSqlDao.class));
+        bind(BusinessSubscriptionTransitionTagSqlDao.class).toProvider(new BusinessSqlProvider<BusinessSubscriptionTransitionTagSqlDao>(BusinessSubscriptionTransitionTagSqlDao.class));
 
         bind(BusinessSubscriptionTransitionRecorder.class).asEagerSingleton();
         bind(BusinessAccountRecorder.class).asEagerSingleton();
+        bind(BusinessTagRecorder.class).asEagerSingleton();
         bind(AnalyticsListener.class).asEagerSingleton();
 
+        bind(AnalyticsDao.class).to(DefaultAnalyticsDao.class).asEagerSingleton();
         bind(AnalyticsService.class).to(DefaultAnalyticsService.class).asEagerSingleton();
+
+        bind(DefaultAnalyticsUserApi.class).asEagerSingleton();
     }
 }
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoiceItemSqlDao.sql.stg b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoiceItemSqlDao.sql.stg
index 44862ea..7952eb6 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoiceItemSqlDao.sql.stg
+++ b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoiceItemSqlDao.sql.stg
@@ -44,6 +44,7 @@ select
 , currency
 from bii
 where invoice_id = :invoice_id
+order by created_date asc
 ;
 >>
 
@@ -67,6 +68,7 @@ select
 , currency
 from bii
 where external_key = :external_key
+order by created_date asc
 ;
 >>
 
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoicePaymentSqlDao.sql.stg b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoicePaymentSqlDao.sql.stg
index e4b800d..c5d0fb8 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoicePaymentSqlDao.sql.stg
+++ b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoicePaymentSqlDao.sql.stg
@@ -46,6 +46,7 @@ select
 , card_country
 from bip
 where payment_id = :payment_id
+order by created_date asc
 ;
 >>
 
@@ -70,6 +71,7 @@ select
 , card_country
 from bip
 where account_key = :account_key
+order by created_date asc
 ;
 >>
 
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoiceSqlDao.sql.stg b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoiceSqlDao.sql.stg
index 7d72ef0..83d3ffc 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoiceSqlDao.sql.stg
+++ b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessInvoiceSqlDao.sql.stg
@@ -34,6 +34,7 @@ select
 , amount_credited
 from bin
 where account_key = :account_key
+order by created_date asc
 ;
 >>
 
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessOverdueStatusSqlDao.sql.stg b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessOverdueStatusSqlDao.sql.stg
index 7153cc2..d412125 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessOverdueStatusSqlDao.sql.stg
+++ b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessOverdueStatusSqlDao.sql.stg
@@ -8,6 +8,7 @@ select
 , end_date
 from bos
 where external_key = :external_key
+order by start_date asc
 ;
 >>
 
diff --git a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
index 2194a1f..82b1241 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
@@ -242,7 +242,7 @@ public class TestAnalyticsService extends TestWithEmbeddedDB {
     }
 
     // STEPH talk to Pierre -- see previous remark hence disable test
-    @Test(groups = "slow", enabled = true)
+    @Test(groups = "slow", enabled = false)
     public void testRegisterForNotifications() throws Exception {
         // Make sure the service has been instantiated
         Assert.assertEquals(service.getName(), "analytics-service");
diff --git a/analytics/src/test/java/com/ning/billing/analytics/dao/TestDefaultAnalyticsDao.java b/analytics/src/test/java/com/ning/billing/analytics/dao/TestDefaultAnalyticsDao.java
new file mode 100644
index 0000000..7ab6a66
--- /dev/null
+++ b/analytics/src/test/java/com/ning/billing/analytics/dao/TestDefaultAnalyticsDao.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010-2012 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.analytics.dao;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.skife.jdbi.v2.IDBI;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.analytics.TestWithEmbeddedDB;
+import com.ning.billing.analytics.model.BusinessAccount;
+import com.ning.billing.analytics.model.BusinessInvoice;
+import com.ning.billing.analytics.model.BusinessInvoiceItem;
+import com.ning.billing.catalog.api.Currency;
+
+public class TestDefaultAnalyticsDao extends TestWithEmbeddedDB {
+    private BusinessAccountSqlDao accountSqlDao;
+    private BusinessInvoiceSqlDao invoiceSqlDao;
+    private BusinessInvoiceItemSqlDao invoiceItemSqlDao;
+    private AnalyticsDao analyticsDao;
+
+    @BeforeMethod(groups = "slow")
+    public void setUp() throws Exception {
+        final IDBI dbi = helper.getDBI();
+        accountSqlDao = dbi.onDemand(BusinessAccountSqlDao.class);
+        final BusinessSubscriptionTransitionSqlDao subscriptionTransitionSqlDao = dbi.onDemand(BusinessSubscriptionTransitionSqlDao.class);
+        invoiceSqlDao = dbi.onDemand(BusinessInvoiceSqlDao.class);
+        invoiceItemSqlDao = dbi.onDemand(BusinessInvoiceItemSqlDao.class);
+        final BusinessAccountTagSqlDao accountTagSqlDao = dbi.onDemand(BusinessAccountTagSqlDao.class);
+        analyticsDao = new DefaultAnalyticsDao(accountSqlDao, subscriptionTransitionSqlDao, invoiceSqlDao, invoiceItemSqlDao, accountTagSqlDao);
+    }
+
+    @Test(groups = "slow")
+    public void testCreateInvoice() throws Exception {
+        // Create and verify the initial state
+        BusinessAccount account = new BusinessAccount(UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+                                                      BigDecimal.ONE, new DateTime(DateTimeZone.UTC), BigDecimal.TEN,
+                                                      "ERROR_NOT_ENOUGH_FUNDS", "CreditCard", "Visa", "FRANCE");
+        Assert.assertEquals(accountSqlDao.createAccount(account), 1);
+        Assert.assertEquals(invoiceSqlDao.getInvoicesForAccount(account.getKey()).size(), 0);
+        account = accountSqlDao.getAccount(account.getKey());
+
+        // Generate the invoices
+        final BusinessInvoice invoice = createInvoice(account.getKey());
+        final List<BusinessInvoiceItem> invoiceItems = new ArrayList<BusinessInvoiceItem>();
+        for (int i = 0; i < 10; i++) {
+            invoiceItems.add(createInvoiceItem(invoice.getInvoiceId(), BigDecimal.valueOf(1242 + i)));
+        }
+        analyticsDao.createInvoice(account.getKey(), invoice, invoiceItems);
+
+        // Verify the final state
+        final List<BusinessInvoice> invoicesForAccount = invoiceSqlDao.getInvoicesForAccount(account.getKey());
+        Assert.assertEquals(invoicesForAccount.size(), 1);
+        Assert.assertEquals(invoicesForAccount.get(0).getInvoiceId(), invoice.getInvoiceId());
+
+        Assert.assertEquals(invoiceItemSqlDao.getInvoiceItemsForInvoice(invoice.getInvoiceId().toString()).size(), 10);
+
+        final BusinessAccount finalAccount = accountSqlDao.getAccount(account.getKey());
+        Assert.assertEquals(finalAccount.getCreatedDt(), account.getCreatedDt());
+        Assert.assertTrue(finalAccount.getUpdatedDt().isAfter(account.getCreatedDt()));
+        Assert.assertTrue(finalAccount.getUpdatedDt().isAfter(account.getUpdatedDt()));
+        Assert.assertTrue(finalAccount.getLastInvoiceDate().equals(invoice.getInvoiceDate()));
+        // invoice.getBalance() is not the sum of all the items here - but in practice it will be
+        Assert.assertEquals(finalAccount.getTotalInvoiceBalance(), account.getTotalInvoiceBalance().add(invoice.getBalance()));
+    }
+
+    private BusinessInvoice createInvoice(final String accountKey) {
+        final BigDecimal amountCharged = BigDecimal.ZERO;
+        final BigDecimal amountCredited = BigDecimal.ONE;
+        final BigDecimal amountPaid = BigDecimal.TEN;
+        final BigDecimal balance = BigDecimal.valueOf(123L);
+        final DateTime createdDate = new DateTime(DateTimeZone.UTC);
+        final Currency currency = Currency.MXN;
+        final DateTime invoiceDate = new DateTime(DateTimeZone.UTC);
+        final UUID invoiceId = UUID.randomUUID();
+        final DateTime targetDate = new DateTime(DateTimeZone.UTC);
+        final DateTime updatedDate = new DateTime(DateTimeZone.UTC);
+
+        return new BusinessInvoice(accountKey, amountCharged, amountCredited, amountPaid, balance,
+                                   createdDate, currency, invoiceDate, invoiceId, targetDate, updatedDate);
+    }
+
+    private BusinessInvoiceItem createInvoiceItem(final UUID invoiceId, final BigDecimal amount) {
+        final String billingPeriod = UUID.randomUUID().toString().substring(0, 20);
+        final DateTime createdDate = new DateTime(DateTimeZone.UTC);
+        final Currency currency = Currency.AUD;
+        final DateTime endDate = new DateTime(DateTimeZone.UTC);
+        final String externalKey = UUID.randomUUID().toString();
+        final UUID itemId = UUID.randomUUID();
+        final String itemType = UUID.randomUUID().toString().substring(0, 20);
+        final String phase = UUID.randomUUID().toString().substring(0, 20);
+        final String productCategory = UUID.randomUUID().toString().substring(0, 20);
+        final String productName = UUID.randomUUID().toString().substring(0, 20);
+        final String productType = UUID.randomUUID().toString().substring(0, 20);
+        final String slug = UUID.randomUUID().toString().substring(0, 20);
+        final DateTime startDate = new DateTime(DateTimeZone.UTC);
+        final DateTime updatedDate = new DateTime(DateTimeZone.UTC);
+
+        return new BusinessInvoiceItem(amount, billingPeriod, createdDate, currency,
+                                       endDate, externalKey, invoiceId, itemId, itemType,
+                                       phase, productCategory, productName, productType,
+                                       slug, startDate, updatedDate);
+    }
+}
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java b/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
index 2d29f08..a5cfc42 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
@@ -74,7 +74,7 @@ public class TestAnalyticsListener extends AnalyticsTestSuite {
     @BeforeMethod(groups = "fast")
     public void setUp() throws Exception {
         final BusinessSubscriptionTransitionRecorder recorder = new BusinessSubscriptionTransitionRecorder(dao, catalogService, new MockEntitlementUserApi(bundleUUID, EXTERNAL_KEY), new MockAccountUserApi(ACCOUNT_KEY, CURRENCY));
-        listener = new AnalyticsListener(recorder, null);
+        listener = new AnalyticsListener(recorder, null, null, null);
     }
 
     @Test(groups = "fast")
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java
new file mode 100644
index 0000000..2eb018b
--- /dev/null
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2012 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.analytics;
+
+import java.util.UUID;
+
+import org.mockito.Mockito;
+import org.skife.jdbi.v2.IDBI;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.account.api.user.DefaultAccountUserApi;
+import com.ning.billing.account.dao.AccountDao;
+import com.ning.billing.account.dao.AccountEmailDao;
+import com.ning.billing.account.dao.AuditedAccountDao;
+import com.ning.billing.account.dao.AuditedAccountEmailDao;
+import com.ning.billing.analytics.dao.BusinessAccountTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoicePaymentTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessInvoiceTagSqlDao;
+import com.ning.billing.analytics.dao.BusinessSubscriptionTransitionTagSqlDao;
+import com.ning.billing.catalog.DefaultCatalogService;
+import com.ning.billing.catalog.api.CatalogService;
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.catalog.io.VersionedCatalogLoader;
+import com.ning.billing.config.CatalogConfig;
+import com.ning.billing.entitlement.alignment.PlanAligner;
+import com.ning.billing.entitlement.api.user.DefaultEntitlementUserApi;
+import com.ning.billing.entitlement.api.user.DefaultSubscriptionApiService;
+import com.ning.billing.entitlement.api.user.DefaultSubscriptionFactory;
+import com.ning.billing.entitlement.api.user.EntitlementUserApi;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.entitlement.engine.addon.AddonUtils;
+import com.ning.billing.entitlement.engine.dao.AuditedEntitlementDao;
+import com.ning.billing.entitlement.engine.dao.EntitlementDao;
+import com.ning.billing.util.bus.InMemoryBus;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContextFactory;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.clock.DefaultClock;
+import com.ning.billing.util.dao.ObjectType;
+import com.ning.billing.util.notificationq.DefaultNotificationQueueService;
+
+public class TestBusinessTagRecorder extends TestWithEmbeddedDB {
+    private BusinessAccountTagSqlDao accountTagSqlDao;
+    private BusinessSubscriptionTransitionTagSqlDao subscriptionTransitionTagSqlDao;
+    private InMemoryBus eventBus;
+    private DefaultCallContextFactory callContextFactory;
+    private AccountUserApi accountUserApi;
+    private EntitlementUserApi entitlementUserApi;
+    private BusinessTagRecorder tagRecorder;
+
+    @BeforeMethod(groups = "slow")
+    public void setUp() throws Exception {
+        final IDBI dbi = helper.getDBI();
+        accountTagSqlDao = dbi.onDemand(BusinessAccountTagSqlDao.class);
+        final BusinessInvoiceTagSqlDao invoiceTagSqlDao = dbi.onDemand(BusinessInvoiceTagSqlDao.class);
+        final BusinessInvoicePaymentTagSqlDao invoicePaymentTagSqlDao = dbi.onDemand(BusinessInvoicePaymentTagSqlDao.class);
+        subscriptionTransitionTagSqlDao = dbi.onDemand(BusinessSubscriptionTransitionTagSqlDao.class);
+        eventBus = new InMemoryBus();
+        final AccountDao accountDao = new AuditedAccountDao(dbi, eventBus);
+        final AccountEmailDao accountEmailDao = new AuditedAccountEmailDao(dbi);
+        final DefaultClock clock = new DefaultClock();
+        callContextFactory = new DefaultCallContextFactory(clock);
+        accountUserApi = new DefaultAccountUserApi(callContextFactory, accountDao, accountEmailDao);
+        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 PlanAligner planAligner = new PlanAligner(catalogService);
+        final DefaultSubscriptionApiService apiService = new DefaultSubscriptionApiService(clock, entitlementDao, catalogService, planAligner);
+        final DefaultSubscriptionFactory subscriptionFactory = new DefaultSubscriptionFactory(apiService, clock, catalogService);
+        entitlementUserApi = new DefaultEntitlementUserApi(clock, entitlementDao, catalogService,
+                                                           apiService, subscriptionFactory, addonUtils);
+        tagRecorder = new BusinessTagRecorder(accountTagSqlDao, invoicePaymentTagSqlDao, invoiceTagSqlDao, subscriptionTransitionTagSqlDao,
+                                              accountUserApi, entitlementUserApi);
+
+        eventBus.start();
+    }
+
+    @AfterMethod(groups = "slow")
+    public void tearDown() throws Exception {
+        eventBus.stop();
+    }
+
+    @Test(groups = "slow")
+    public void testAddAndRemoveTagsForAccount() throws Exception {
+        final String name = UUID.randomUUID().toString().substring(0, 20);
+        final CallContext callContext = callContextFactory.createCallContext(UUID.randomUUID().toString(), CallOrigin.TEST, UserType.TEST);
+        final String accountKey = UUID.randomUUID().toString();
+
+        final Account account = accountUserApi.createAccount(new MockAccount(UUID.randomUUID(), accountKey, Currency.MXN), callContext);
+        final UUID accountId = account.getId();
+
+        Assert.assertEquals(accountTagSqlDao.getTagsForAccount(accountKey).size(), 0);
+        tagRecorder.tagAdded(ObjectType.ACCOUNT, accountId, name);
+        Assert.assertEquals(accountTagSqlDao.getTagsForAccount(accountKey).size(), 1);
+        tagRecorder.tagRemoved(ObjectType.ACCOUNT, accountId, name);
+        Assert.assertEquals(accountTagSqlDao.getTagsForAccount(accountKey).size(), 0);
+    }
+
+    @Test(groups = "slow")
+    public void testAddAndRemoveTagsForBundle() throws Exception {
+        final String name = UUID.randomUUID().toString().substring(0, 20);
+        final CallContext callContext = callContextFactory.createCallContext(UUID.randomUUID().toString(), CallOrigin.TEST, UserType.TEST);
+        final String externalKey = UUID.randomUUID().toString();
+
+        final Account account = accountUserApi.createAccount(new MockAccount(UUID.randomUUID(), UUID.randomUUID().toString(), Currency.MXN), callContext);
+        final SubscriptionBundle bundle = entitlementUserApi.createBundleForAccount(account.getId(), externalKey, callContext);
+        final UUID bundleId = bundle.getId();
+
+        Assert.assertEquals(subscriptionTransitionTagSqlDao.getTagsForBusinessSubscriptionTransition(externalKey).size(), 0);
+        tagRecorder.tagAdded(ObjectType.BUNDLE, bundleId, name);
+        Assert.assertEquals(subscriptionTransitionTagSqlDao.getTagsForBusinessSubscriptionTransition(externalKey).size(), 1);
+        tagRecorder.tagRemoved(ObjectType.BUNDLE, bundleId, name);
+        Assert.assertEquals(subscriptionTransitionTagSqlDao.getTagsForBusinessSubscriptionTransition(externalKey).size(), 0);
+    }
+}

beatrix/pom.xml 5(+5 -0)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 96d50b9..78690e8 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -108,6 +108,11 @@
         </dependency>
         <dependency>
             <groupId>com.ning.billing</groupId>
+            <artifactId>killbill-analytics</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.ning.billing</groupId>
             <artifactId>killbill-overdue</artifactId>
             <scope>test</scope>
         </dependency>
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixModule.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixModule.java
index 0e0ea55..c26fac5 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixModule.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/BeatrixModule.java
@@ -29,6 +29,7 @@ import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.ning.billing.account.api.AccountService;
 import com.ning.billing.account.glue.AccountModule;
+import com.ning.billing.analytics.setup.AnalyticsModule;
 import com.ning.billing.beatrix.integration.overdue.IntegrationTestOverdueModule;
 import com.ning.billing.beatrix.lifecycle.DefaultLifecycle;
 import com.ning.billing.beatrix.lifecycle.Lifecycle;
@@ -96,6 +97,7 @@ public class BeatrixModule extends AbstractModule {
         install(new TagStoreModule());
         install(new CustomFieldModule());
         install(new AccountModule());
+        install(new AnalyticsModule());
         install(new CatalogModule());
         install(new DefaultEntitlementModule());
         install(new DefaultInvoiceModule());
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
new file mode 100644
index 0000000..9dadb9e
--- /dev/null
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2010-2012 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.beatrix.integration;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.account.api.AccountData;
+import com.ning.billing.account.api.MutableAccountData;
+import com.ning.billing.analytics.model.BusinessAccount;
+import com.ning.billing.analytics.model.BusinessAccountTag;
+import com.ning.billing.analytics.model.BusinessInvoice;
+import com.ning.billing.analytics.model.BusinessInvoiceItem;
+import com.ning.billing.analytics.model.BusinessSubscriptionEvent;
+import com.ning.billing.analytics.model.BusinessSubscriptionTransition;
+import com.ning.billing.analytics.utils.Rounder;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.CatalogApiException;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
+import com.ning.billing.entitlement.api.user.Subscription;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.util.api.TagApiException;
+import com.ning.billing.util.api.TagDefinitionApiException;
+import com.ning.billing.util.dao.ObjectType;
+import com.ning.billing.util.tag.TagDefinition;
+
+@Guice(modules = BeatrixModule.class)
+public class TestAnalytics extends TestIntegrationBase {
+    @BeforeMethod(groups = "slow")
+    public void setUpAnalyticsHandler() throws Exception {
+        busService.getBus().register(analyticsListener);
+    }
+
+    @AfterMethod(groups = "slow")
+    public void tearDownAnalyticsHandler() throws Exception {
+        busService.getBus().unregister(analyticsListener);
+    }
+
+    @Test(groups = "slow")
+    public void testAnalyticsEvents() throws Exception {
+        // Create an account
+        final Account account = verifyAccountCreation();
+
+        // Update some fields
+        verifyAccountUpdate(account);
+
+        // Add a tag
+        verifyAddTagToAccount(account);
+
+        // Create a bundle
+        final SubscriptionBundle bundle = verifyFirstBundle(account);
+
+        // Add a subscription
+        final Subscription subscription = verifyFirstSubscription(account, bundle);
+
+        // Upgrade the subscription
+        verifyChangePlan(account, bundle, subscription);
+    }
+
+    private Account verifyAccountCreation() throws Exception {
+        final AccountData accountData = getAccountData(1);
+
+        // Verify BAC is empty
+        Assert.assertNull(analyticsUserApi.getAccountByKey(accountData.getExternalKey()));
+
+        // Create an account
+        final Account account = createAccountWithPaymentMethod(accountData);
+        Assert.assertNotNull(account);
+
+        waitALittle();
+
+        // Verify Analytics got the account creation event
+        final BusinessAccount businessAccount = analyticsUserApi.getAccountByKey(account.getExternalKey());
+        Assert.assertNotNull(businessAccount);
+        // No balance yet
+        Assert.assertEquals(businessAccount.getBalance().doubleValue(), Rounder.round(BigDecimal.ZERO));
+        Assert.assertEquals(businessAccount.getKey(), account.getExternalKey());
+        // No invoice yet
+        Assert.assertNull(businessAccount.getLastInvoiceDate());
+        // No payment yet
+        Assert.assertNull(businessAccount.getLastPaymentStatus());
+        Assert.assertEquals(businessAccount.getName(), account.getName());
+        // No invoice balance yet
+        Assert.assertEquals(businessAccount.getTotalInvoiceBalance().doubleValue(), Rounder.round(BigDecimal.ZERO));
+        // TODO - payment fields
+        //Assert.assertNotNull(businessAccount.getBillingAddressCountry());
+        //Assert.assertNotNull(businessAccount.getCreditCardType());
+        //Assert.assertNotNull(businessAccount.getPaymentMethod());
+
+        // The account shouldn't have any invoice yet
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).size(), 0);
+
+        return account;
+    }
+
+    private void verifyAccountUpdate(final Account account) throws InterruptedException {
+        final MutableAccountData mutableAccountData = account.toMutableAccountData();
+
+        mutableAccountData.setName(UUID.randomUUID().toString().substring(0, 20));
+
+        try {
+            accountUserApi.updateAccount(account.getId(), mutableAccountData, context);
+        } catch (AccountApiException e) {
+            Assert.fail("Unable to update account", e);
+        }
+
+        waitALittle();
+
+        // Verify Analytics got the account update event
+        final BusinessAccount businessAccount = analyticsUserApi.getAccountByKey(mutableAccountData.getExternalKey());
+        Assert.assertNotNull(businessAccount);
+        // No balance yet
+        Assert.assertEquals(businessAccount.getBalance().doubleValue(), Rounder.round(BigDecimal.ZERO));
+        Assert.assertEquals(businessAccount.getKey(), mutableAccountData.getExternalKey());
+        // No invoice yet
+        Assert.assertNull(businessAccount.getLastInvoiceDate());
+        // No payment yet
+        Assert.assertNull(businessAccount.getLastPaymentStatus());
+        Assert.assertEquals(businessAccount.getName(), mutableAccountData.getName());
+        // No invoice balance yet
+        Assert.assertEquals(businessAccount.getTotalInvoiceBalance().doubleValue(), Rounder.round(BigDecimal.ZERO));
+        // TODO - payment fields
+        //Assert.assertNotNull(businessAccount.getBillingAddressCountry());
+        //Assert.assertNotNull(businessAccount.getCreditCardType());
+        //Assert.assertNotNull(businessAccount.getPaymentMethod());
+
+        // The account should still not have any invoice
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).size(), 0);
+    }
+
+    private void verifyAddTagToAccount(final Account account) throws TagDefinitionApiException, TagApiException, InterruptedException {
+        Assert.assertEquals(analyticsUserApi.getTagsForAccount(account.getExternalKey()).size(), 0);
+
+        final TagDefinition tagDefinition = tagUserApi.create(UUID.randomUUID().toString().substring(0, 10), UUID.randomUUID().toString(), context);
+        tagUserApi.addTag(account.getId(), ObjectType.ACCOUNT, tagDefinition, context);
+
+        waitALittle();
+
+        final List<BusinessAccountTag> tagsForAccount = analyticsUserApi.getTagsForAccount(account.getExternalKey());
+        Assert.assertEquals(tagsForAccount.size(), 1);
+        Assert.assertEquals(tagsForAccount.get(0).getName(), tagDefinition.getName());
+    }
+
+    private SubscriptionBundle verifyFirstBundle(final Account account) throws EntitlementUserApiException, InterruptedException {
+        // Add a bundle
+        final SubscriptionBundle bundle = entitlementUserApi.createBundleForAccount(account.getId(), UUID.randomUUID().toString(), context);
+        Assert.assertNotNull(bundle);
+
+        waitALittle();
+
+        // Verify BST is still empty since no subscription has been added yet
+        Assert.assertEquals(analyticsUserApi.getTransitionsForBundle(bundle.getKey()).size(), 0);
+
+        // The account should still not have any invoice
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).size(), 0);
+
+        return bundle;
+    }
+
+    private Subscription verifyFirstSubscription(final Account account, final SubscriptionBundle bundle) throws EntitlementUserApiException, InterruptedException, CatalogApiException {
+        // Add a subscription
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+        final String planSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+        final PlanPhaseSpecifier phaseSpecifier = new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSetName, null);
+        final Subscription subscription = entitlementUserApi.createSubscription(bundle.getId(), phaseSpecifier, null, context);
+
+        waitALittle();
+
+        // BST should have one transition
+        final List<BusinessSubscriptionTransition> transitions = analyticsUserApi.getTransitionsForBundle(bundle.getKey());
+        Assert.assertEquals(transitions.size(), 1);
+        final BusinessSubscriptionTransition transition = transitions.get(0);
+        Assert.assertEquals(transition.getExternalKey(), bundle.getKey());
+        Assert.assertEquals(transition.getAccountKey(), account.getExternalKey());
+        Assert.assertEquals(transition.getEvent().getCategory(), phaseSpecifier.getProductCategory());
+        Assert.assertEquals(transition.getEvent().getEventType(), BusinessSubscriptionEvent.EventType.ADD);
+
+        // This is the first transition
+        Assert.assertNull(transition.getPreviousSubscription());
+
+        Assert.assertEquals(transition.getNextSubscription().getBillingPeriod(), subscription.getCurrentPhase().getBillingPeriod().toString());
+        Assert.assertEquals(transition.getNextSubscription().getBundleId(), subscription.getBundleId());
+        Assert.assertEquals(transition.getNextSubscription().getCurrency(), account.getCurrency().toString());
+        Assert.assertEquals(transition.getNextSubscription().getPhase(), subscription.getCurrentPhase().getPhaseType().toString());
+        // Trial: fixed price of zero
+        Assert.assertEquals(transition.getNextSubscription().getPrice().doubleValue(), subscription.getCurrentPhase().getFixedPrice().getPrice(account.getCurrency()).doubleValue());
+        Assert.assertEquals(transition.getNextSubscription().getPriceList(), subscription.getCurrentPriceList().getName());
+        Assert.assertEquals(transition.getNextSubscription().getProductCategory(), subscription.getCurrentPlan().getProduct().getCategory());
+        Assert.assertEquals(transition.getNextSubscription().getProductName(), subscription.getCurrentPlan().getProduct().getName());
+        Assert.assertEquals(transition.getNextSubscription().getProductType(), subscription.getCurrentPlan().getProduct().getCatalogName());
+        Assert.assertEquals(transition.getNextSubscription().getSlug(), subscription.getCurrentPhase().getName());
+        Assert.assertEquals(transition.getNextSubscription().getStartDate(), subscription.getStartDate());
+        Assert.assertEquals(transition.getNextSubscription().getState(), subscription.getState());
+        Assert.assertEquals(transition.getNextSubscription().getSubscriptionId(), subscription.getId());
+
+        // Make sure the account balance is still zero
+        final BusinessAccount businessAccount = analyticsUserApi.getAccountByKey(account.getExternalKey());
+        Assert.assertEquals(businessAccount.getBalance().doubleValue(), Rounder.round(BigDecimal.ZERO));
+        Assert.assertEquals(businessAccount.getTotalInvoiceBalance().doubleValue(), Rounder.round(BigDecimal.ZERO));
+
+        // The account should have one invoice for the trial phase
+        final List<BusinessInvoice> invoices = analyticsUserApi.getInvoicesForAccount(account.getExternalKey());
+        Assert.assertEquals(invoices.size(), 1);
+        final BusinessInvoice invoice = invoices.get(0);
+        Assert.assertEquals(invoice.getBalance().doubleValue(), 0.0);
+        Assert.assertEquals(invoice.getAmountCharged().doubleValue(), 0.0);
+        Assert.assertEquals(invoice.getAmountCredited().doubleValue(), 0.0);
+        Assert.assertEquals(invoice.getAmountPaid().doubleValue(), 0.0);
+        Assert.assertEquals(invoice.getCurrency(), account.getCurrency());
+
+        // The invoice should have a single item associated to it
+        final List<BusinessInvoiceItem> invoiceItems = analyticsUserApi.getInvoiceItemsForInvoice(invoice.getInvoiceId());
+        Assert.assertEquals(invoiceItems.size(), 1);
+        final BusinessInvoiceItem invoiceItem = invoiceItems.get(0);
+        Assert.assertEquals(invoiceItem.getAmount().doubleValue(), 0.0);
+        // No billing period for the trial item
+        Assert.assertEquals(invoiceItem.getBillingPeriod(), subscription.getCurrentPhase().getBillingPeriod().toString());
+        Assert.assertEquals(invoiceItem.getCurrency(), account.getCurrency());
+        // The subscription end date is null (evergreen)
+        Assert.assertEquals(invoiceItem.getEndDate(), subscription.getStartDate().plus(subscription.getCurrentPhase().getDuration().toJodaPeriod()));
+        Assert.assertEquals(invoiceItem.getExternalKey(), bundle.getKey());
+        Assert.assertEquals(invoiceItem.getInvoiceId(), invoice.getInvoiceId());
+        Assert.assertEquals(invoiceItem.getItemType(), "FIXED");
+        Assert.assertEquals(invoiceItem.getPhase(), subscription.getCurrentPhase().getPhaseType().toString());
+        Assert.assertEquals(invoiceItem.getProductCategory(), subscription.getCurrentPlan().getProduct().getCategory().toString());
+        Assert.assertEquals(invoiceItem.getProductName(), subscription.getCurrentPlan().getProduct().getName());
+        Assert.assertEquals(invoiceItem.getProductType(), subscription.getCurrentPlan().getProduct().getCatalogName());
+        Assert.assertEquals(invoiceItem.getSlug(), subscription.getCurrentPhase().getName());
+        Assert.assertEquals(invoiceItem.getStartDate(), subscription.getStartDate());
+
+        return subscription;
+    }
+
+    private void verifyChangePlan(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws EntitlementUserApiException, InterruptedException {
+        final String newProductName = "Assault-Rifle";
+        final BillingPeriod newTerm = BillingPeriod.MONTHLY;
+        final String newPlanSetName = PriceListSet.DEFAULT_PRICELIST_NAME;
+        final DateTime requestedDate = clock.getUTCNow();
+        Assert.assertTrue(subscription.changePlan(newProductName, newTerm, newPlanSetName, requestedDate, context));
+
+        waitALittle();
+
+        // BST should have two transitions
+        final List<BusinessSubscriptionTransition> transitions = analyticsUserApi.getTransitionsForBundle(bundle.getKey());
+        Assert.assertEquals(transitions.size(), 2);
+        final BusinessSubscriptionTransition previousTransition = transitions.get(0);
+        final BusinessSubscriptionTransition transition = transitions.get(1);
+        Assert.assertEquals(transition.getExternalKey(), bundle.getKey());
+        Assert.assertEquals(transition.getAccountKey(), account.getExternalKey());
+        Assert.assertEquals(transition.getEvent().getCategory(), ProductCategory.BASE);
+        Assert.assertEquals(transition.getEvent().getEventType(), BusinessSubscriptionEvent.EventType.CHANGE);
+
+        // Verify the previous subscription matches
+        Assert.assertNull(previousTransition.getPreviousSubscription());
+        Assert.assertEquals(previousTransition.getNextSubscription(), transition.getPreviousSubscription());
+
+        // Verify the next subscription
+        // No billing period for the trial phase
+        Assert.assertEquals(transition.getNextSubscription().getBillingPeriod(), BillingPeriod.NO_BILLING_PERIOD.toString());
+        Assert.assertEquals(transition.getNextSubscription().getBundleId(), subscription.getBundleId());
+        Assert.assertEquals(transition.getNextSubscription().getCurrency(), account.getCurrency().toString());
+        Assert.assertEquals(transition.getNextSubscription().getPhase(), PhaseType.TRIAL.toString());
+        // We're still in trial
+        Assert.assertEquals(transition.getNextSubscription().getPrice().doubleValue(), 0.0);
+        Assert.assertEquals(transition.getNextSubscription().getPriceList(), newPlanSetName);
+        Assert.assertEquals(transition.getNextSubscription().getProductCategory(), ProductCategory.BASE);
+        Assert.assertEquals(transition.getNextSubscription().getProductName(), newProductName);
+        Assert.assertEquals(transition.getNextSubscription().getProductType(), subscription.getCurrentPlan().getProduct().getCatalogName());
+        Assert.assertEquals(transition.getNextSubscription().getSlug(), subscription.getCurrentPhase().getName());
+        Assert.assertEquals(transition.getNextSubscription().getStartDate(), requestedDate);
+        Assert.assertEquals(transition.getNextSubscription().getState(), Subscription.SubscriptionState.ACTIVE);
+        // It's still the same subscription
+        Assert.assertEquals(transition.getNextSubscription().getSubscriptionId(), subscription.getId());
+
+        // The account should have two invoices for the trial phase of both subscriptions
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).size(), 2);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(0).getBalance().doubleValue(), 0.0);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(0).getAmountCharged().doubleValue(), 0.0);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(0).getAmountCredited().doubleValue(), 0.0);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(0).getAmountPaid().doubleValue(), 0.0);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(0).getCurrency(), account.getCurrency());
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(1).getBalance().doubleValue(), 0.0);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(1).getAmountCharged().doubleValue(), 0.0);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(1).getAmountCredited().doubleValue(), 0.0);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(1).getAmountPaid().doubleValue(), 0.0);
+        Assert.assertEquals(analyticsUserApi.getInvoicesForAccount(account.getExternalKey()).get(1).getCurrency(), account.getCurrency());
+    }
+
+    private void waitALittle() throws InterruptedException {
+        // We especially need to wait for entitlement events
+        Thread.sleep(2000);
+    }
+}
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
index 43ccf5d..d17b0b2 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestIntegrationBase.java
@@ -37,6 +37,8 @@ import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountData;
 import com.ning.billing.account.api.AccountService;
 import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.analytics.AnalyticsListener;
+import com.ning.billing.analytics.api.user.DefaultAnalyticsUserApi;
 import com.ning.billing.api.TestApiListener;
 import com.ning.billing.api.TestListenerStatus;
 import com.ning.billing.beatrix.lifecycle.Lifecycle;
@@ -56,6 +58,7 @@ import com.ning.billing.invoice.model.InvoicingConfiguration;
 import com.ning.billing.junction.plumbing.api.BlockingSubscription;
 import com.ning.billing.payment.api.PaymentApi;
 import com.ning.billing.payment.api.PaymentMethodPlugin;
+import com.ning.billing.util.api.TagUserApi;
 import com.ning.billing.util.bus.BusService;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallOrigin;
@@ -124,6 +127,15 @@ public class TestIntegrationBase implements TestListenerStatus {
     @Inject
     protected AccountUserApi accountUserApi;
 
+    @Inject
+    protected DefaultAnalyticsUserApi analyticsUserApi;
+
+    @Inject
+    protected TagUserApi tagUserApi;
+
+    @Inject
+    protected AnalyticsListener analyticsListener;
+
     protected TestApiListener busHandler;
 
 
@@ -152,6 +164,7 @@ public class TestIntegrationBase implements TestListenerStatus {
 
     protected void setupMySQL() throws IOException {
         final String accountDdl = IOUtils.toString(TestIntegration.class.getResourceAsStream("/com/ning/billing/account/ddl.sql"));
+        final String analyticsDdl = IOUtils.toString(TestIntegration.class.getResourceAsStream("/com/ning/billing/analytics/ddl.sql"));
         final String entitlementDdl = IOUtils.toString(TestIntegration.class.getResourceAsStream("/com/ning/billing/entitlement/ddl.sql"));
         final String invoiceDdl = IOUtils.toString(TestIntegration.class.getResourceAsStream("/com/ning/billing/invoice/ddl.sql"));
         final String paymentDdl = IOUtils.toString(TestIntegration.class.getResourceAsStream("/com/ning/billing/payment/ddl.sql"));
@@ -161,6 +174,7 @@ public class TestIntegrationBase implements TestListenerStatus {
         helper.startMysql();
 
         helper.initDb(accountDdl);
+        helper.initDb(analyticsDdl);
         helper.initDb(entitlementDdl);
         helper.initDb(invoiceDdl);
         helper.initDb(paymentDdl);