killbill-aplcache

analytics: add API to refresh the state for an account This

10/10/2012 2:52:56 PM

Details

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
index fb74122..840f483 100644
--- 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
@@ -16,11 +16,27 @@
 
 package com.ning.billing.analytics.api.user;
 
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.analytics.BusinessAccountDao;
+import com.ning.billing.analytics.BusinessInvoiceDao;
+import com.ning.billing.analytics.BusinessInvoicePaymentDao;
+import com.ning.billing.analytics.BusinessOverdueStatusDao;
+import com.ning.billing.analytics.BusinessSubscriptionTransitionDao;
+import com.ning.billing.analytics.BusinessTagDao;
 import com.ning.billing.analytics.api.TimeSeriesData;
 import com.ning.billing.analytics.dao.AnalyticsDao;
 import com.ning.billing.analytics.model.BusinessAccount;
@@ -30,17 +46,61 @@ import com.ning.billing.analytics.model.BusinessInvoiceItem;
 import com.ning.billing.analytics.model.BusinessInvoicePayment;
 import com.ning.billing.analytics.model.BusinessOverdueStatus;
 import com.ning.billing.analytics.model.BusinessSubscriptionTransition;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.junction.api.Blockable.Type;
+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.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.InternalCallContext;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.dao.ObjectType;
+import com.ning.billing.util.svcapi.entitlement.EntitlementInternalApi;
+import com.ning.billing.util.svcapi.tag.TagInternalApi;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Sets;
 
 public class DefaultAnalyticsUserApi implements AnalyticsUserApi {
 
-    private final InternalCallContextFactory internalCallContextFactory;
+    private static final Logger log = LoggerFactory.getLogger(DefaultAnalyticsUserApi.class);
+
     private final AnalyticsDao analyticsDao;
+    private final BusinessSubscriptionTransitionDao bstDao;
+    private final BusinessAccountDao bacDao;
+    private final BusinessInvoiceDao invoiceDao;
+    private final BusinessOverdueStatusDao bosDao;
+    private final BusinessInvoicePaymentDao bipDao;
+    private final BusinessTagDao tagDao;
+    private final EntitlementInternalApi entitlementInternalApi;
+    private final PaymentApi paymentApi;
+    private final TagInternalApi tagInternalApi;
+    private final InternalCallContextFactory internalCallContextFactory;
 
     @Inject
-    public DefaultAnalyticsUserApi(final AnalyticsDao analyticsDao, final InternalCallContextFactory internalCallContextFactory) {
+    public DefaultAnalyticsUserApi(final AnalyticsDao analyticsDao,
+                                   final BusinessSubscriptionTransitionDao bstDao,
+                                   final BusinessAccountDao bacDao,
+                                   final BusinessInvoiceDao invoiceDao,
+                                   final BusinessOverdueStatusDao bosDao,
+                                   final BusinessInvoicePaymentDao bipDao,
+                                   final BusinessTagDao tagDao,
+                                   final EntitlementInternalApi entitlementInternalApi,
+                                   final PaymentApi paymentApi,
+                                   final TagInternalApi tagInternalApi,
+                                   final InternalCallContextFactory internalCallContextFactory) {
         this.analyticsDao = analyticsDao;
+        this.bstDao = bstDao;
+        this.bacDao = bacDao;
+        this.invoiceDao = invoiceDao;
+        this.bosDao = bosDao;
+        this.bipDao = bipDao;
+        this.tagDao = tagDao;
+        this.entitlementInternalApi = entitlementInternalApi;
+        this.paymentApi = paymentApi;
+        this.tagInternalApi = tagInternalApi;
         this.internalCallContextFactory = internalCallContextFactory;
     }
 
@@ -54,6 +114,158 @@ public class DefaultAnalyticsUserApi implements AnalyticsUserApi {
         return analyticsDao.getSubscriptionsCreatedOverTime(productType, slug, internalCallContextFactory.createInternalTenantContext(context));
     }
 
+    @Override
+    public void rebuildAnalyticsForAccount(final Account account, final CallContext context) {
+        final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(context);
+
+        // Update the BAC row
+        bacDao.accountUpdated(account.getId(), internalCallContext);
+
+        // Update BST for all bundles
+        final Set<UUID> bundleIds = updateBST(account, internalCallContext);
+
+        // Update BIN and BII for all invoices
+        invoiceDao.rebuildInvoicesForAccount(account.getId(), internalCallContext);
+
+        // Update BIP for all invoices
+        try {
+            updateBIP(account, context, internalCallContext);
+        } catch (PaymentApiException e) {
+            // Log and ignore
+            log.warn(e.toString());
+        }
+
+        // Update BOS for all bundles (only blockable supported today)
+        // TODO: support other blockables
+        for (final UUID bundleId : bundleIds) {
+            bosDao.overdueStatusChanged(Type.SUBSCRIPTION_BUNDLE, bundleId, internalCallContext);
+        }
+
+        // Update bac_tags
+        // TODO: refresh all tags
+        updateTags(account, internalCallContext);
+    }
+
+    private Set<UUID> updateBST(final Account account, final InternalCallContext internalCallContext) {
+        // Find the current state of bundles in entitlement
+        final Collection<UUID> entitlementBundlesId = Collections2.transform(entitlementInternalApi.getBundlesForAccount(account.getId(), internalCallContext),
+                                                                             new Function<SubscriptionBundle, UUID>() {
+                                                                                 @Override
+                                                                                 public UUID apply(@Nullable final SubscriptionBundle input) {
+                                                                                     if (input == null) {
+                                                                                         return null;
+                                                                                     } else {
+                                                                                         return input.getId();
+                                                                                     }
+                                                                                 }
+                                                                             });
+
+        // Find the current state of bundles in analytics
+        final Collection<UUID> analyticsBundlesId = Collections2.transform(analyticsDao.getTransitionsForAccount(account.getExternalKey(), internalCallContext),
+                                                                           new Function<BusinessSubscriptionTransition, UUID>() {
+                                                                               @Override
+                                                                               public UUID apply(@Nullable final BusinessSubscriptionTransition input) {
+                                                                                   if (input == null) {
+                                                                                       return null;
+                                                                                   } else {
+                                                                                       return input.getBundleId();
+                                                                                   }
+                                                                               }
+                                                                           });
+
+        // Update BST for all bundles found
+        final Set<UUID> bundlesId = new HashSet<UUID>();
+        bundlesId.addAll(entitlementBundlesId);
+        bundlesId.addAll(analyticsBundlesId);
+        for (final UUID bundleId : bundlesId) {
+            bstDao.rebuildTransitionsForBundle(bundleId, internalCallContext);
+        }
+
+        return bundlesId;
+    }
+
+    private void updateBIP(final Account account, final TenantContext tenantContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+        final List<Payment> accountPayments = paymentApi.getAccountPayments(account.getId(), tenantContext);
+        final Map<UUID, Payment> payments = new HashMap<UUID, Payment>();
+        for (final Payment payment : accountPayments) {
+            payments.put(payment.getId(), payment);
+        }
+
+        // Find the current state of payments in payment
+        final Collection<UUID> paymentPaymentsId = Collections2.transform(accountPayments,
+                                                                          new Function<Payment, UUID>() {
+                                                                              @Override
+                                                                              public UUID apply(@Nullable final Payment input) {
+                                                                                  if (input == null) {
+                                                                                      return null;
+                                                                                  } else {
+                                                                                      return input.getId();
+                                                                                  }
+                                                                              }
+                                                                          });
+
+        // Find the current state of payments in analytics
+        final Collection<UUID> analyticsPaymentsId = Collections2.transform(analyticsDao.getInvoicePaymentsForAccountByKey(account.getExternalKey(), internalCallContext),
+                                                                            new Function<BusinessInvoicePayment, UUID>() {
+                                                                                @Override
+                                                                                public UUID apply(@Nullable final BusinessInvoicePayment input) {
+                                                                                    if (input == null) {
+                                                                                        return null;
+                                                                                    } else {
+                                                                                        return input.getPaymentId();
+                                                                                    }
+                                                                                }
+                                                                            });
+
+        // Update BIP for all payments found
+        final Set<UUID> paymentsId = new HashSet<UUID>();
+        paymentsId.addAll(paymentPaymentsId);
+        paymentsId.addAll(analyticsPaymentsId);
+        for (final UUID paymentId : paymentsId) {
+            final Payment paymentInfo = payments.get(paymentId);
+            bipDao.invoicePaymentPosted(paymentInfo.getAccountId(),
+                                        paymentInfo.getId(),
+                                        paymentInfo.getExtFirstPaymentIdRef(),
+                                        paymentInfo.getExtSecondPaymentIdRef(),
+                                        paymentInfo.getPaymentStatus().toString(),
+                                        internalCallContext);
+        }
+    }
+
+    private void updateTags(final Account account, final InternalCallContext internalCallContext) {
+        // Find the current state of tags from util
+        final Collection<String> utilTags = Collections2.transform(tagInternalApi.getTags(account.getId(), ObjectType.ACCOUNT, internalCallContext).keySet(),
+                                                                   new Function<String, String>() {
+                                                                       @Override
+                                                                       public String apply(@Nullable final String input) {
+                                                                           return input;
+                                                                       }
+                                                                   });
+
+        // Find the current state of tags in analytics
+        final Collection<String> analyticsTags = Collections2.transform(analyticsDao.getTagsForAccount(account.getExternalKey(), internalCallContext),
+                                                                        new Function<BusinessAccountTag, String>() {
+                                                                            @Override
+                                                                            public String apply(@Nullable final BusinessAccountTag input) {
+                                                                                if (input == null) {
+                                                                                    return null;
+                                                                                } else {
+                                                                                    return input.getName();
+                                                                                }
+                                                                            }
+                                                                        });
+
+        // Remove non existing tags
+        for (final String tag : Sets.difference(new HashSet<String>(analyticsTags), new HashSet<String>(utilTags))) {
+            tagDao.tagRemoved(ObjectType.ACCOUNT, account.getId(), tag, internalCallContext);
+        }
+
+        // Add missing ones
+        for (final String tag : Sets.difference(new HashSet<String>(utilTags), new HashSet<String>(analyticsTags))) {
+            tagDao.tagAdded(ObjectType.ACCOUNT, account.getId(), tag, internalCallContext);
+        }
+    }
+
     // Note: the following is not exposed in api yet, as the models need to be extracted first
 
     public BusinessAccount getAccountByKey(final String accountKey, final TenantContext context) {
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/AnalyticsDao.java b/analytics/src/main/java/com/ning/billing/analytics/dao/AnalyticsDao.java
index d99f5b8..7c4ab6c 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/AnalyticsDao.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/AnalyticsDao.java
@@ -38,6 +38,8 @@ public interface AnalyticsDao {
 
     List<BusinessSubscriptionTransition> getTransitionsByKey(String externalKey, InternalTenantContext context);
 
+    List<BusinessSubscriptionTransition> getTransitionsForAccount(String accountKey, InternalTenantContext context);
+
     List<BusinessInvoice> getInvoicesByKey(String accountKey, InternalTenantContext context);
 
     List<BusinessAccountTag> getTagsForAccount(String accountKey, InternalTenantContext context);
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
index 6706537..0e65c7b 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
@@ -47,6 +47,10 @@ public interface BusinessSubscriptionTransitionSqlDao extends Transactional<Busi
     List<BusinessSubscriptionTransition> getTransitionForSubscription(@Bind("subscription_id") final String subscriptionId,
                                                                       @InternalTenantContextBinder final InternalTenantContext context);
 
+    @SqlQuery
+    List<BusinessSubscriptionTransition> getTransitionsForAccount(@Bind("account_key") final String accountKey,
+                                                                  @InternalTenantContextBinder final InternalTenantContext context);
+
     @SqlUpdate
     int createTransition(@BusinessSubscriptionTransitionBinder final BusinessSubscriptionTransition transition,
                          @InternalTenantContextBinder final InternalCallContext context);
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
index 59e9af8..e91c988 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/DefaultAnalyticsDao.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/DefaultAnalyticsDao.java
@@ -79,6 +79,11 @@ public class DefaultAnalyticsDao implements AnalyticsDao {
     }
 
     @Override
+    public List<BusinessSubscriptionTransition> getTransitionsForAccount(final String accountKey, final InternalTenantContext context) {
+        return subscriptionTransitionSqlDao.getTransitionsForAccount(accountKey, context);
+    }
+
+    @Override
     public List<BusinessInvoice> getInvoicesByKey(final String accountKey, final InternalTenantContext context) {
         return invoiceSqlDao.getInvoicesForAccountByKey(accountKey, context);
     }
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
index 8649840..8f85dd6 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
+++ b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
@@ -101,6 +101,48 @@ getTransitionForSubscription(subscription_id) ::= <<
   ;
 >>
 
+getTransitionsForAccount(account_id) ::= <<
+  select
+    total_ordering
+  , bundle_id
+  , external_key
+  , account_id
+  , account_key
+  , subscription_id
+  , requested_timestamp
+  , event
+  , prev_product_name
+  , prev_product_type
+  , prev_product_category
+  , prev_slug
+  , prev_phase
+  , prev_billing_period
+  , prev_price
+  , prev_price_list
+  , prev_mrr
+  , prev_currency
+  , prev_start_date
+  , prev_state
+  , next_product_name
+  , next_product_type
+  , next_product_category
+  , next_slug
+  , next_phase
+  , next_billing_period
+  , next_price
+  , next_price_list
+  , next_mrr
+  , next_currency
+  , next_start_date
+  , next_state
+  , tenant_record_id
+  from bst
+  where account_key = :account_key
+  <AND_CHECK_TENANT()>
+  order by requested_timestamp asc
+  ;
+>>
+
 createTransition() ::= <<
   insert into bst(
     total_ordering
diff --git a/analytics/src/test/java/com/ning/billing/analytics/api/user/TestDefaultAnalyticsUserApi.java b/analytics/src/test/java/com/ning/billing/analytics/api/user/TestDefaultAnalyticsUserApi.java
index 70cb427..905aa2a 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/api/user/TestDefaultAnalyticsUserApi.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/api/user/TestDefaultAnalyticsUserApi.java
@@ -27,6 +27,12 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.ning.billing.analytics.AnalyticsTestSuiteWithEmbeddedDB;
+import com.ning.billing.analytics.BusinessAccountDao;
+import com.ning.billing.analytics.BusinessInvoiceDao;
+import com.ning.billing.analytics.BusinessInvoicePaymentDao;
+import com.ning.billing.analytics.BusinessOverdueStatusDao;
+import com.ning.billing.analytics.BusinessSubscriptionTransitionDao;
+import com.ning.billing.analytics.BusinessTagDao;
 import com.ning.billing.analytics.MockDuration;
 import com.ning.billing.analytics.MockPhase;
 import com.ning.billing.analytics.MockProduct;
@@ -53,10 +59,13 @@ import com.ning.billing.catalog.api.Product;
 import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.mock.MockPlan;
+import com.ning.billing.payment.api.PaymentApi;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.ClockMock;
+import com.ning.billing.util.svcapi.entitlement.EntitlementInternalApi;
+import com.ning.billing.util.svcapi.tag.TagInternalApi;
 
 public class TestDefaultAnalyticsUserApi extends AnalyticsTestSuiteWithEmbeddedDB {
 
@@ -85,7 +94,17 @@ public class TestDefaultAnalyticsUserApi extends AnalyticsTestSuiteWithEmbeddedD
 
         final AnalyticsDao analyticsDao = new DefaultAnalyticsDao(accountSqlDao, subscriptionTransitionSqlDao, invoiceSqlDao,
                                                                   invoiceItemSqlDao, accountTagSqlDao, overdueStatusSqlDao, invoicePaymentSqlDao);
-        analyticsUserApi = new DefaultAnalyticsUserApi(analyticsDao, new InternalCallContextFactory(dbi, clock));
+        analyticsUserApi = new DefaultAnalyticsUserApi(analyticsDao,
+                                                       Mockito.mock(BusinessSubscriptionTransitionDao.class),
+                                                       Mockito.mock(BusinessAccountDao.class),
+                                                       Mockito.mock(BusinessInvoiceDao.class),
+                                                       Mockito.mock(BusinessOverdueStatusDao.class),
+                                                       Mockito.mock(BusinessInvoicePaymentDao.class),
+                                                       Mockito.mock(BusinessTagDao.class),
+                                                       Mockito.mock(EntitlementInternalApi.class),
+                                                       Mockito.mock(PaymentApi.class),
+                                                       Mockito.mock(TagInternalApi.class),
+                                                       new InternalCallContextFactory(dbi, clock));
     }
 
     @Test(groups = "slow")
diff --git a/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java b/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
index 58d3148..1cc7c31 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
@@ -60,6 +60,11 @@ public class MockBusinessSubscriptionTransitionSqlDao implements BusinessSubscri
     }
 
     @Override
+    public List<BusinessSubscriptionTransition> getTransitionsForAccount(@Bind("account_id") final String accountId, @InternalTenantContextBinder final InternalTenantContext context) {
+        return ImmutableList.<BusinessSubscriptionTransition>of();
+    }
+
+    @Override
     public int createTransition(@BusinessSubscriptionTransitionBinder final BusinessSubscriptionTransition transition,
                                 @InternalTenantContextBinder final InternalCallContext context) {
         if (content.get(transition.getExternalKey()) == null) {
diff --git a/api/src/main/java/com/ning/billing/analytics/api/user/AnalyticsUserApi.java b/api/src/main/java/com/ning/billing/analytics/api/user/AnalyticsUserApi.java
index cbdea9b..ba5ae0f 100644
--- a/api/src/main/java/com/ning/billing/analytics/api/user/AnalyticsUserApi.java
+++ b/api/src/main/java/com/ning/billing/analytics/api/user/AnalyticsUserApi.java
@@ -16,7 +16,9 @@
 
 package com.ning.billing.analytics.api.user;
 
+import com.ning.billing.account.api.Account;
 import com.ning.billing.analytics.api.TimeSeriesData;
+import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
 
 public interface AnalyticsUserApi {
@@ -32,4 +34,12 @@ public interface AnalyticsUserApi {
      * @return the number of new subscriptions created per day (transfers not included)
      */
     public TimeSeriesData getSubscriptionsCreatedOverTime(String productType, String slug, TenantContext context);
+
+    /**
+     * Rebuild all analytics tables for an account
+     *
+     * @param account account
+     * @param context call context
+     */
+    void rebuildAnalyticsForAccount(Account account, CallContext context);
 }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AnalyticsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AnalyticsResource.java
index e8c871e..bbf996f 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AnalyticsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AnalyticsResource.java
@@ -16,15 +16,23 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.util.UUID;
+
 import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 
+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.api.TimeSeriesData;
 import com.ning.billing.analytics.api.user.AnalyticsUserApi;
 import com.ning.billing.jaxrs.json.TimeSeriesDataJson;
@@ -33,6 +41,7 @@ import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
 import com.ning.billing.util.api.AuditUserApi;
 import com.ning.billing.util.api.CustomFieldUserApi;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.callcontext.CallContext;
 
 import com.google.inject.Singleton;
 
@@ -42,19 +51,35 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 @Path(JaxrsResource.ANALYTICS_PATH)
 public class AnalyticsResource extends JaxRsResourceBase {
 
+    private final AccountUserApi accountUserApi;
     private final AnalyticsUserApi analyticsUserApi;
 
     @Inject
-    public AnalyticsResource(final AnalyticsUserApi analyticsUserApi,
+    public AnalyticsResource(final AccountUserApi accountUserApi,
+                             final AnalyticsUserApi analyticsUserApi,
                              final JaxrsUriBuilder uriBuilder,
                              final TagUserApi tagUserApi,
                              final CustomFieldUserApi customFieldUserApi,
                              final AuditUserApi auditUserApi,
                              final Context context) {
         super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, context);
+        this.accountUserApi = accountUserApi;
         this.analyticsUserApi = analyticsUserApi;
     }
 
+    @PUT
+    @Path("/{accountId:" + UUID_PATTERN + "}")
+    public Response rebuildAnalyticsForAccount(@PathParam("accountId") final String accountId,
+                                               @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                               @HeaderParam(HDR_REASON) final String reason,
+                                               @HeaderParam(HDR_COMMENT) final String comment,
+                                               @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+        final Account account = accountUserApi.getAccountById(UUID.fromString(accountId), callContext);
+        analyticsUserApi.rebuildAnalyticsForAccount(account, callContext);
+        return Response.status(Status.OK).build();
+    }
+
     @GET
     @Path("/accountsCreatedOverTime")
     @Produces(APPLICATION_JSON)