diff --git a/internal/worker/jobs/handler.go b/internal/worker/jobs/handler.go index cd046e2..c8940c0 100644 --- a/internal/worker/jobs/handler.go +++ b/internal/worker/jobs/handler.go @@ -34,6 +34,7 @@ const ( type Handler struct { db *gorm.DB taskRepo *repositories.TaskRepository + residenceRepo *repositories.ResidenceRepository reminderRepo *repositories.ReminderRepository pushClient *push.Client emailService *services.EmailService @@ -53,6 +54,7 @@ func NewHandler(db *gorm.DB, pushClient *push.Client, emailService *services.Ema return &Handler{ db: db, taskRepo: repositories.NewTaskRepository(db), + residenceRepo: repositories.NewResidenceRepository(db), reminderRepo: repositories.NewReminderRepository(db), pushClient: pushClient, emailService: emailService, @@ -275,8 +277,6 @@ func (h *Handler) HandleDailyDigest(ctx context.Context, task *asynq.Task) error now := time.Now().UTC() currentHour := now.Hour() systemDefaultHour := h.config.Worker.DailyNotifHour - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) - nextWeek := today.AddDate(0, 0, 7) log.Info().Int("current_hour", currentHour).Int("system_default_hour", systemDefaultHour).Msg("Daily digest check") @@ -312,54 +312,49 @@ func (h *Handler) HandleDailyDigest(ctx context.Context, task *asynq.Task) error log.Info().Int("eligible_users", len(eligibleUserIDs)).Msg("Found users eligible for daily digest this hour") - // Step 2: Get task statistics only for eligible users - // Completion detection logic matches internal/task/predicates.IsCompleted: - // A task is "completed" when NextDueDate == nil AND has at least one completion. - // We use COALESCE(next_due_date, due_date) as the effective date for categorization. - var userStats []struct { - UserID uint - TotalTasks int - OverdueTasks int - DueThisWeek int - } + // Step 2: Get task statistics for each user using canonical repository functions + // This ensures consistency with kanban display - uses the same scopes: + // - task.ScopeOverdue (excludes in-progress tasks) + // - task.ScopeDueSoon (7 days for "due this week") + var usersNotified int - err = h.db.Raw(` - SELECT - u.id as user_id, - COUNT(DISTINCT t.id) as total_tasks, - COUNT(DISTINCT CASE - WHEN COALESCE(t.next_due_date, t.due_date) < ? - AND NOT (t.next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = t.id)) - THEN t.id - END) as overdue_tasks, - COUNT(DISTINCT CASE - WHEN COALESCE(t.next_due_date, t.due_date) >= ? AND COALESCE(t.next_due_date, t.due_date) < ? - AND NOT (t.next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = t.id)) - THEN t.id - END) as due_this_week - FROM auth_user u - JOIN residence_residence r ON (r.owner_id = u.id OR r.id IN ( - SELECT residence_id FROM residence_residence_users WHERE user_id = u.id - )) AND r.is_active = true - JOIN task_task t ON t.residence_id = r.id - AND t.is_cancelled = false - AND t.is_archived = false - WHERE u.is_active = true AND u.id IN ? - GROUP BY u.id - HAVING COUNT(DISTINCT t.id) > 0 - `, today, today, nextWeek, eligibleUserIDs).Scan(&userStats).Error + for _, userID := range eligibleUserIDs { + // Get user's residence IDs (owned + member) + residenceIDs, err := h.residenceRepo.FindResidenceIDsByUser(userID) + if err != nil { + log.Error().Err(err).Uint("user_id", userID).Msg("Failed to get user residences") + continue + } - if err != nil { - log.Error().Err(err).Msg("Failed to query user task statistics") - return err - } + if len(residenceIDs) == 0 { + continue // User has no residences + } - log.Info().Int("users_with_tasks", len(userStats)).Msg("Processing daily digest for users") + // Query overdue tasks using canonical scopes (excludes in-progress) + opts := repositories.TaskFilterOptions{ + ResidenceIDs: residenceIDs, + IncludeInProgress: false, // Match kanban: in-progress tasks not in overdue column + PreloadCompletions: true, + } + + overdueTasks, err := h.taskRepo.GetOverdueTasks(now, opts) + if err != nil { + log.Error().Err(err).Uint("user_id", userID).Msg("Failed to get overdue tasks") + continue + } + + // Query due-this-week tasks (7 days threshold) + dueSoonTasks, err := h.taskRepo.GetDueSoonTasks(now, 7, opts) + if err != nil { + log.Error().Err(err).Uint("user_id", userID).Msg("Failed to get due-soon tasks") + continue + } + + overdueCount := len(overdueTasks) + dueThisWeekCount := len(dueSoonTasks) - // Step 3: Send notifications - for _, stats := range userStats { // Skip users with no actionable items - if stats.OverdueTasks == 0 && stats.DueThisWeek == 0 { + if overdueCount == 0 && dueThisWeekCount == 0 { continue } @@ -367,25 +362,27 @@ func (h *Handler) HandleDailyDigest(ctx context.Context, task *asynq.Task) error title := "Daily Task Summary" var body string - if stats.OverdueTasks > 0 && stats.DueThisWeek > 0 { - body = fmt.Sprintf("You have %d overdue task(s) and %d task(s) due this week", stats.OverdueTasks, stats.DueThisWeek) - } else if stats.OverdueTasks > 0 { - body = fmt.Sprintf("You have %d overdue task(s) that need attention", stats.OverdueTasks) + if overdueCount > 0 && dueThisWeekCount > 0 { + body = fmt.Sprintf("You have %d overdue task(s) and %d task(s) due this week", overdueCount, dueThisWeekCount) + } else if overdueCount > 0 { + body = fmt.Sprintf("You have %d overdue task(s) that need attention", overdueCount) } else { - body = fmt.Sprintf("You have %d task(s) due this week", stats.DueThisWeek) + body = fmt.Sprintf("You have %d task(s) due this week", dueThisWeekCount) } // Send push notification - if err := h.sendPushToUser(ctx, stats.UserID, title, body, map[string]string{ + if err := h.sendPushToUser(ctx, userID, title, body, map[string]string{ "type": "daily_digest", - "overdue": fmt.Sprintf("%d", stats.OverdueTasks), - "due_this_week": fmt.Sprintf("%d", stats.DueThisWeek), + "overdue": fmt.Sprintf("%d", overdueCount), + "due_this_week": fmt.Sprintf("%d", dueThisWeekCount), }); err != nil { - log.Error().Err(err).Uint("user_id", stats.UserID).Msg("Failed to send daily digest push") + log.Error().Err(err).Uint("user_id", userID).Msg("Failed to send daily digest push") + } else { + usersNotified++ } } - log.Info().Int("users_notified", len(userStats)).Msg("Daily digest notifications completed") + log.Info().Int("users_notified", usersNotified).Msg("Daily digest notifications completed") return nil }