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>
92 lines
3.1 KiB
Go
92 lines
3.1 KiB
Go
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"
|
|
}
|