private async Task <bool> SendSummaryNotificationAsync(Project project, SummaryNotification data)
        {
            // TODO: Add slack daily summaries
            var userIds = project.NotificationSettings.Where(n => n.Value.SendDailySummary && !String.Equals(n.Key, Project.NotificationIntegrations.Slack)).Select(n => n.Key).ToList();

            if (userIds.Count == 0)
            {
                _logger.LogInformation("Project {ProjectName} has no users to send summary to.", project.Name);
                return(false);
            }

            var results = await _userRepository.GetByIdsAsync(userIds, o => o.Cache()).AnyContext();

            var users = results.Where(u => u.IsEmailAddressVerified && u.EmailNotificationsEnabled && u.OrganizationIds.Contains(project.OrganizationId)).ToList();

            if (users.Count == 0)
            {
                _logger.LogInformation("Project {ProjectName} has no users to send summary to.", project.Name);
                return(false);
            }

            // TODO: What should we do about suspended organizations.
            var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()).AnyContext();

            if (organization == null)
            {
                _logger.LogInformation("The organization {organization} for project {ProjectName} may have been deleted. No summaries will be sent.", project.OrganizationId, project.Name);
                return(false);
            }

            _logger.LogInformation("Sending daily summary: users={UserCount} project={project}", users.Count, project.Id);
            var    sf           = new AppFilter(project, organization);
            var    systemFilter = new RepositoryQuery <PersistentEvent>().AppFilter(sf).DateRange(data.UtcStartTime, data.UtcEndTime, (PersistentEvent e) => e.Date).Index(data.UtcStartTime, data.UtcEndTime);
            string filter       = "type:error (status:open OR status:regressed)";
            var    result       = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression("terms:(first @include:true) terms:(stack_id~3) cardinality:stack_id sum:count~1")).AnyContext();

            double total              = result.Aggregations.Sum("sum_count")?.Value ?? result.Total;
            double newTotal           = result.Aggregations.Terms <double>("terms_first")?.Buckets.FirstOrDefault()?.Total ?? 0;
            double uniqueTotal        = result.Aggregations.Cardinality("cardinality_stack_id")?.Value ?? 0;
            bool   hasSubmittedEvents = total > 0 || project.IsConfigured.GetValueOrDefault();
            bool   isFreePlan         = organization.PlanId == _plans.FreePlan.Id;

            string fixedFilter = "type:error status:fixed";
            var    fixedResult = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(fixedFilter).EnforceEventStackFilter().AggregationsExpression("sum:count~1")).AnyContext();

            double fixedTotal = fixedResult.Aggregations.Sum("sum_count")?.Value ?? fixedResult.Total;

            var range        = new DateTimeRange(data.UtcStartTime, data.UtcEndTime);
            var usages       = project.OverageHours.Where(u => range.Contains(u.Date)).ToList();
            int blockedTotal = usages.Sum(u => u.Blocked);
            int tooBigTotal  = usages.Sum(u => u.TooBig);

            IReadOnlyCollection <Stack> mostFrequent = null;
            var stackTerms = result.Aggregations.Terms <string>("terms_stack_id");

            if (stackTerms?.Buckets.Count > 0)
            {
                mostFrequent = await _stackRepository.GetByIdsAsync(stackTerms.Buckets.Select(b => b.Key).ToArray()).AnyContext();
            }

            IReadOnlyCollection <Stack> newest = null;

            if (newTotal > 0)
            {
                newest = (await _stackRepository.FindAsync(q => q.AppFilter(sf).FilterExpression(filter).SortExpression("-first").DateRange(data.UtcStartTime, data.UtcEndTime, "first"), o => o.PageLimit(3)).AnyContext()).Documents;
            }

            foreach (var user in users)
            {
                _logger.LogInformation("Queuing {ProjectName} daily summary email ({UtcStartTime}-{UtcEndTime}) for user {EmailAddress}.", project.Name, data.UtcStartTime, data.UtcEndTime, user.EmailAddress);
                await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, newest, data.UtcStartTime, hasSubmittedEvents, total, uniqueTotal, newTotal, fixedTotal, blockedTotal, tooBigTotal, isFreePlan).AnyContext();
            }

            _logger.LogInformation("Done sending daily summary: users={UserCount} project={ProjectName} events={EventCount}", users.Count, project.Name, total);
            return(true);
        }
        private async Task <bool> SendSummaryNotificationAsync(Project project, SummaryNotification data)
        {
            // TODO: Add slack daily summaries
            var userIds = project.NotificationSettings.Where(n => n.Value.SendDailySummary && !String.Equals(n.Key, Project.NotificationIntegrations.Slack)).Select(n => n.Key).ToList();

            if (userIds.Count == 0)
            {
                _logger.Info().Project(project.Id).Message("Project \"{0}\" has no users to send summary to.", project.Name).Write();
                return(false);
            }

            var results = await _userRepository.GetByIdsAsync(userIds, o => o.Cache()).AnyContext();

            var users = results.Where(u => u.IsEmailAddressVerified && u.EmailNotificationsEnabled && u.OrganizationIds.Contains(project.OrganizationId)).ToList();

            if (users.Count == 0)
            {
                _logger.Info().Project(project.Id).Message("Project \"{0}\" has no users to send summary to.", project.Name);
                return(false);
            }

            // TODO: What should we do about suspended organizations.
            var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()).AnyContext();

            if (organization == null)
            {
                _logger.Info().Project(project.Id).Message("The organization \"{0}\" for project \"{1}\" may have been deleted. No summaries will be sent.", project.OrganizationId, project.Name);
                return(false);
            }

            _logger.Info("Sending daily summary: users={0} project={1}", users.Count, project.Id);
            var    sf           = new ExceptionlessSystemFilter(project, organization);
            var    systemFilter = new RepositoryQuery <PersistentEvent>().SystemFilter(sf).DateRange(data.UtcStartTime, data.UtcEndTime, (PersistentEvent e) => e.Date).Index(data.UtcStartTime, data.UtcEndTime);
            string filter       = $"{EventIndexType.Alias.Type}:{Event.KnownTypes.Error} {EventIndexType.Alias.IsHidden}:false {EventIndexType.Alias.IsFixed}:false";
            var    result       = await _eventRepository.CountBySearchAsync(systemFilter, filter, "terms:(first @include:true) terms:(stack_id~3) cardinality:stack_id sum:count~1").AnyContext();

            double total              = result.Aggregations.Sum("sum_count").Value ?? result.Total;
            double newTotal           = result.Aggregations.Terms <double>("terms_first")?.Buckets.FirstOrDefault()?.Total ?? 0;
            double uniqueTotal        = result.Aggregations.Cardinality("cardinality_stack_id")?.Value ?? 0;
            bool   hasSubmittedEvents = total > 0 || project.IsConfigured.GetValueOrDefault();
            bool   isFreePlan         = organization.PlanId == BillingManager.FreePlan.Id;

            string fixedFilter = $"{EventIndexType.Alias.Type}:{Event.KnownTypes.Error} {EventIndexType.Alias.IsHidden}:false {EventIndexType.Alias.IsFixed}:true";
            var    fixedResult = await _eventRepository.CountBySearchAsync(systemFilter, fixedFilter, "sum:count~1").AnyContext();

            double fixedTotal = fixedResult.Aggregations.Sum("sum_count").Value ?? fixedResult.Total;

            var range        = new DateTimeRange(data.UtcStartTime, data.UtcEndTime);
            var usages       = project.OverageHours.Where(u => range.Contains(u.Date)).ToList();
            int blockedTotal = usages.Sum(u => u.Blocked);
            int tooBigTotal  = usages.Sum(u => u.TooBig);

            IReadOnlyCollection <Stack> mostFrequent = null;
            var stackTerms = result.Aggregations.Terms <string>("terms_stack_id");

            if (stackTerms?.Buckets.Count > 0)
            {
                mostFrequent = await _stackRepository.GetByIdsAsync(stackTerms.Buckets.Select(b => b.Key).ToArray()).AnyContext();
            }

            IReadOnlyCollection <Stack> newest = null;

            if (newTotal > 0)
            {
                newest = (await _stackRepository.GetByFilterAsync(sf, filter, "-first", "first", data.UtcStartTime, data.UtcEndTime, o => o.PageLimit(3)).AnyContext()).Documents;
            }

            foreach (var user in users)
            {
                _logger.Info().Project(project.Id).Message("Queuing \"{0}\" daily summary email ({1}-{2}) for user {3}.", project.Name, data.UtcStartTime, data.UtcEndTime, user.EmailAddress);
                await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, newest, data.UtcStartTime, hasSubmittedEvents, total, uniqueTotal, newTotal, fixedTotal, blockedTotal, tooBigTotal, isFreePlan).AnyContext();
            }

            _logger.Info().Project(project.Id).Message("Done sending daily summary: users={0} project={1} events={2}", users.Count, project.Name, total);
            return(true);
        }