Add smart notification reminder system with frequency-aware scheduling
Replaces one-size-fits-all "2 days before" reminders with intelligent
scheduling based on task frequency. Infrequent tasks (annual) get 30-day
advance notice while frequent tasks (weekly) only get day-of reminders.
Key features:
- Frequency-aware pre-reminders: annual (30d, 14d, 7d), quarterly (7d, 3d),
monthly (3d), bi-weekly (1d), daily/weekly/once (day-of only)
- Overdue tapering: daily for 3 days, then every 3 days, stops after 14 days
- Reminder log table prevents duplicate notifications per due date/stage
- Admin endpoint displays notification schedules for all frequencies
- Comprehensive test suite (100 random tasks, 61 days each, 10 test functions)
New files:
- internal/notifications/reminder_config.go - Editable schedule configuration
- internal/notifications/reminder_schedule.go - Schedule lookup logic
- internal/notifications/reminder_schedule_test.go - Dynamic test suite
- internal/models/reminder_log.go - TaskReminderLog model
- internal/repositories/reminder_repo.go - Reminder log repository
- migrations/010_add_task_reminder_log.{up,down}.sql
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
91
internal/models/reminder_log.go
Normal file
91
internal/models/reminder_log.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReminderStage represents the stage of a task reminder
|
||||
type ReminderStage string
|
||||
|
||||
const (
|
||||
// Pre-due date reminders (days before)
|
||||
ReminderStage30Days ReminderStage = "reminder_30d"
|
||||
ReminderStage14Days ReminderStage = "reminder_14d"
|
||||
ReminderStage7Days ReminderStage = "reminder_7d"
|
||||
ReminderStage3Days ReminderStage = "reminder_3d"
|
||||
ReminderStage1Day ReminderStage = "reminder_1d"
|
||||
ReminderStageDayOf ReminderStage = "day_of"
|
||||
|
||||
// Overdue reminders (days after)
|
||||
ReminderStageOverdue1 ReminderStage = "overdue_1"
|
||||
ReminderStageOverdue2 ReminderStage = "overdue_2"
|
||||
ReminderStageOverdue3 ReminderStage = "overdue_3"
|
||||
ReminderStageOverdue4 ReminderStage = "overdue_4"
|
||||
ReminderStageOverdue7 ReminderStage = "overdue_7"
|
||||
ReminderStageOverdue10 ReminderStage = "overdue_10"
|
||||
ReminderStageOverdue13 ReminderStage = "overdue_13"
|
||||
)
|
||||
|
||||
// GetReminderStageForDaysBefore returns the reminder stage for days before due date
|
||||
func GetReminderStageForDaysBefore(days int) ReminderStage {
|
||||
switch days {
|
||||
case 30:
|
||||
return ReminderStage30Days
|
||||
case 14:
|
||||
return ReminderStage14Days
|
||||
case 7:
|
||||
return ReminderStage7Days
|
||||
case 3:
|
||||
return ReminderStage3Days
|
||||
case 1:
|
||||
return ReminderStage1Day
|
||||
case 0:
|
||||
return ReminderStageDayOf
|
||||
default:
|
||||
// For any other number of days, create a custom stage
|
||||
return ReminderStage(fmt.Sprintf("reminder_%dd", days))
|
||||
}
|
||||
}
|
||||
|
||||
// GetReminderStageForDaysOverdue returns the reminder stage for days overdue
|
||||
func GetReminderStageForDaysOverdue(days int) ReminderStage {
|
||||
switch days {
|
||||
case 1:
|
||||
return ReminderStageOverdue1
|
||||
case 2:
|
||||
return ReminderStageOverdue2
|
||||
case 3:
|
||||
return ReminderStageOverdue3
|
||||
case 4:
|
||||
return ReminderStageOverdue4
|
||||
case 7:
|
||||
return ReminderStageOverdue7
|
||||
case 10:
|
||||
return ReminderStageOverdue10
|
||||
case 13:
|
||||
return ReminderStageOverdue13
|
||||
default:
|
||||
// For any other overdue day, format as overdue_N
|
||||
return ReminderStage(fmt.Sprintf("overdue_%d", days))
|
||||
}
|
||||
}
|
||||
|
||||
// TaskReminderLog tracks which reminders have been sent to prevent duplicates
|
||||
type TaskReminderLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TaskID uint `gorm:"column:task_id;not null;index:idx_reminderlog_task_user_date" json:"task_id"`
|
||||
Task *Task `gorm:"foreignKey:TaskID" json:"-"`
|
||||
UserID uint `gorm:"column:user_id;not null;index:idx_reminderlog_task_user_date" json:"user_id"`
|
||||
User *User `gorm:"foreignKey:UserID" json:"-"`
|
||||
DueDate time.Time `gorm:"column:due_date;type:date;not null;index:idx_reminderlog_task_user_date" json:"due_date"`
|
||||
ReminderStage ReminderStage `gorm:"column:reminder_stage;size:20;not null" json:"reminder_stage"`
|
||||
SentAt time.Time `gorm:"column:sent_at;default:CURRENT_TIMESTAMP;index:idx_reminderlog_sent_at" json:"sent_at"`
|
||||
NotificationID *uint `gorm:"column:notification_id" json:"notification_id"`
|
||||
Notification *Notification `gorm:"foreignKey:NotificationID" json:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for GORM
|
||||
func (TaskReminderLog) TableName() string {
|
||||
return "task_reminderlog"
|
||||
}
|
||||
Reference in New Issue
Block a user