package repositories import ( "time" "gorm.io/gorm" "github.com/treytartt/honeydue-api/internal/models" ) // ReminderRepository handles database operations for task reminder logs type ReminderRepository struct { db *gorm.DB } // NewReminderRepository creates a new reminder repository func NewReminderRepository(db *gorm.DB) *ReminderRepository { return &ReminderRepository{db: db} } // HasSentReminder checks if a reminder has already been sent for the given // task, user, due date, and reminder stage. func (r *ReminderRepository) HasSentReminder(taskID, userID uint, dueDate time.Time, stage models.ReminderStage) (bool, error) { // Normalize to date only dueDateOnly := time.Date(dueDate.Year(), dueDate.Month(), dueDate.Day(), 0, 0, 0, 0, time.UTC) var count int64 err := r.db.Model(&models.TaskReminderLog{}). Where("task_id = ? AND user_id = ? AND due_date = ? AND reminder_stage = ?", taskID, userID, dueDateOnly, stage). Count(&count).Error if err != nil { return false, err } return count > 0, nil } // ReminderKey uniquely identifies a reminder that may have been sent. type ReminderKey struct { TaskID uint UserID uint DueDate time.Time Stage models.ReminderStage } // HasSentReminderBatch checks which reminders from the given list have already been sent. // Returns a set of indices into the input slice that have already been sent. // This replaces N individual HasSentReminder calls with a single query. func (r *ReminderRepository) HasSentReminderBatch(keys []ReminderKey) (map[int]bool, error) { result := make(map[int]bool) if len(keys) == 0 { return result, nil } // Build a lookup from (task_id, user_id, due_date, stage) -> index type normalizedKey struct { TaskID uint UserID uint DueDate string Stage models.ReminderStage } keyToIdx := make(map[normalizedKey][]int, len(keys)) // Collect unique task IDs and user IDs for the WHERE clause taskIDSet := make(map[uint]bool) userIDSet := make(map[uint]bool) for i, k := range keys { taskIDSet[k.TaskID] = true userIDSet[k.UserID] = true dueDateOnly := time.Date(k.DueDate.Year(), k.DueDate.Month(), k.DueDate.Day(), 0, 0, 0, 0, time.UTC) nk := normalizedKey{ TaskID: k.TaskID, UserID: k.UserID, DueDate: dueDateOnly.Format("2006-01-02"), Stage: k.Stage, } keyToIdx[nk] = append(keyToIdx[nk], i) } taskIDs := make([]uint, 0, len(taskIDSet)) for id := range taskIDSet { taskIDs = append(taskIDs, id) } userIDs := make([]uint, 0, len(userIDSet)) for id := range userIDSet { userIDs = append(userIDs, id) } // Query all matching reminder logs in one query var logs []models.TaskReminderLog err := r.db.Where("task_id IN ? AND user_id IN ?", taskIDs, userIDs). Find(&logs).Error if err != nil { return nil, err } // Match returned logs against our key set for _, l := range logs { dueDateStr := l.DueDate.Format("2006-01-02") nk := normalizedKey{ TaskID: l.TaskID, UserID: l.UserID, DueDate: dueDateStr, Stage: l.ReminderStage, } if indices, ok := keyToIdx[nk]; ok { for _, idx := range indices { result[idx] = true } } } return result, nil } // LogReminder records that a reminder was sent. // Returns the created log entry or an error if the reminder was already sent // (unique constraint violation). func (r *ReminderRepository) LogReminder(taskID, userID uint, dueDate time.Time, stage models.ReminderStage, notificationID *uint) (*models.TaskReminderLog, error) { // Normalize to date only dueDateOnly := time.Date(dueDate.Year(), dueDate.Month(), dueDate.Day(), 0, 0, 0, 0, time.UTC) log := &models.TaskReminderLog{ TaskID: taskID, UserID: userID, DueDate: dueDateOnly, ReminderStage: stage, SentAt: time.Now().UTC(), NotificationID: notificationID, } err := r.db.Create(log).Error if err != nil { return nil, err } return log, nil } // GetSentRemindersForTask returns all reminder logs for a specific task and user. func (r *ReminderRepository) GetSentRemindersForTask(taskID, userID uint) ([]models.TaskReminderLog, error) { var logs []models.TaskReminderLog err := r.db.Where("task_id = ? AND user_id = ?", taskID, userID). Order("sent_at DESC"). Find(&logs).Error return logs, err } // GetSentRemindersForDueDate returns all reminder logs for a specific task, // user, and due date. func (r *ReminderRepository) GetSentRemindersForDueDate(taskID, userID uint, dueDate time.Time) ([]models.TaskReminderLog, error) { dueDateOnly := time.Date(dueDate.Year(), dueDate.Month(), dueDate.Day(), 0, 0, 0, 0, time.UTC) var logs []models.TaskReminderLog err := r.db.Where("task_id = ? AND user_id = ? AND due_date = ?", taskID, userID, dueDateOnly). Order("sent_at DESC"). Find(&logs).Error return logs, err } // CleanupOldLogs removes reminder logs older than the specified number of days. // This helps keep the table from growing indefinitely. func (r *ReminderRepository) CleanupOldLogs(daysOld int) (int64, error) { cutoff := time.Now().UTC().AddDate(0, 0, -daysOld) result := r.db.Where("sent_at < ?", cutoff). Delete(&models.TaskReminderLog{}) return result.RowsAffected, result.Error } // GetRecentReminderStats returns statistics about recent reminders sent. // Useful for admin/monitoring purposes. func (r *ReminderRepository) GetRecentReminderStats(sinceHours int) (map[string]int64, error) { since := time.Now().UTC().Add(-time.Duration(sinceHours) * time.Hour) stats := make(map[string]int64) // Count by stage rows, err := r.db.Model(&models.TaskReminderLog{}). Select("reminder_stage, COUNT(*) as count"). Where("sent_at >= ?", since). Group("reminder_stage"). Rows() if err != nil { return nil, err } defer rows.Close() for rows.Next() { var stage string var count int64 if err := rows.Scan(&stage, &count); err != nil { return nil, err } stats[stage] = count } return stats, nil }