using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; using Microsoft.EntityFrameworkCore; using NodaTime; using NodaTime.TimeZones; namespace DysonNetwork.Pass.Rewind; /// /// Although the pass uses the rewind service call internally, no need for grpc. /// But we created a service that produce the grpc type for consistency. /// public class PassRewindService(AppDatabase db) { public async Task CreateRewindEvent(Guid accountId, int year) { var startDate = new LocalDate(year - 1, 12, 26).AtMidnight().InUtc().ToInstant(); var endDate = new LocalDate(year, 12, 26).AtMidnight().InUtc().ToInstant(); var timeZone = (await db.AccountProfiles .Where(p => p.AccountId == accountId) .FirstOrDefaultAsync())?.TimeZone; var zone = TimeZoneInfo.Utc; if (!string.IsNullOrEmpty(timeZone)) try { zone = TimeZoneInfo.FindSystemTimeZoneById(timeZone); } catch (DateTimeZoneNotFoundException) { // use UTC } var newFriendsCount = await db.AccountRelationships .Where(r => r.CreatedAt >= startDate && r.CreatedAt < endDate) .Where(r => r.AccountId == accountId) .Where(r => r.Status == RelationshipStatus.Friends) .CountAsync(); var newBlockedCount = await db.AccountRelationships .Where(r => r.CreatedAt >= startDate && r.CreatedAt < endDate) .Where(r => r.AccountId == accountId) .Where(r => r.Status == RelationshipStatus.Blocked) .CountAsync(); var checkInDates = await db.AccountCheckInResults .Where(a => a.CreatedAt >= startDate && a.CreatedAt < endDate) .Where(a => a.AccountId == accountId) .Select(a => a.CreatedAt.ToDateTimeUtc().Date) .Distinct() .OrderBy(d => d) .ToListAsync(); var maxCheckInStreak = 0; if (checkInDates.Count != 0) { maxCheckInStreak = checkInDates .Select((d, i) => new { Date = d, Index = i }) .GroupBy(x => x.Date.Subtract(new TimeSpan(x.Index, 0, 0, 0))) .Select(g => g.Count()) .Max(); } var checkInCompleteness = checkInDates.Count / 365.0; var actionDates = await db.ActionLogs .Where(a => a.CreatedAt >= startDate && a.CreatedAt < endDate) .Where(a => a.AccountId == accountId) .Select(a => a.CreatedAt.ToDateTimeUtc().Date) .ToListAsync(); var mostActiveDay = actionDates .GroupBy(d => d) .Select(g => new { Date = g.Key, Count = g.Count() }) .OrderByDescending(g => g.Count) .FirstOrDefault(); var mostActiveWeekday = actionDates .GroupBy(d => d.DayOfWeek) .Select(g => new { Day = g.Key, Count = g.Count() }) .OrderByDescending(g => g.Count) .FirstOrDefault(); var actionTimes = actionDates .Select(a => TimeZoneInfo.ConvertTimeFromUtc(a, zone).TimeOfDay) .ToList(); TimeSpan? latestActiveTime = null; if (actionTimes.Count != 0) { var timesBefore6Am = actionTimes.Where(t => t < TimeSpan.FromHours(6)).ToList(); latestActiveTime = timesBefore6Am.Count != 0 ? timesBefore6Am.Max() : actionTimes.Max(); } var lotteriesQuery = db.Lotteries .Where(l => l.CreatedAt >= startDate && l.CreatedAt < endDate) .Where(l => l.AccountId == accountId) .AsQueryable(); var lotteriesWins = await lotteriesQuery .Where(l => l.MatchedRegionOneNumbers != null && l.MatchedRegionOneNumbers.Count > 0) .CountAsync(); var lotteriesLosses = await lotteriesQuery .Where(l => l.MatchedRegionOneNumbers == null || l.MatchedRegionOneNumbers.Count == 0) .CountAsync(); var lotteriesWinRate = lotteriesWins / (double)(lotteriesWins + lotteriesLosses); var data = new Dictionary { ["max_check_in_streak"] = maxCheckInStreak, ["check_in_completeness"] = checkInCompleteness, ["most_active_day"] = mostActiveDay?.Date.ToString("yyyy-MM-dd"), ["most_active_weekday"] = mostActiveWeekday?.Day.ToString(), ["latest_active_time"] = latestActiveTime?.ToString(@"hh\:mm"), ["new_friends_count"] = newFriendsCount, ["new_blocked_count"] = newBlockedCount, ["lotteries_wins"] = lotteriesWins, ["lotteries_losses"] = lotteriesLosses, ["lotteries_win_rate"] = lotteriesWinRate, }; return new RewindEvent { ServiceId = "pass", AccountId = accountId.ToString(), Data = GrpcTypeHelper.ConvertObjectToByteString(data) }; } }