Fix date comparison for cross-DB compatibility and add timezone coverage
- Change all date scopes from PostgreSQL-specific ::date to DATE() function which works in both PostgreSQL and SQLite (used in tests) - Fix ScopeOverdue, ScopeDueSoon, ScopeUpcoming, ScopeDueInRange - Fix GetOverdueTasks inline query in task_repo.go - Fix timezone unit tests: due dates must be stored as midnight UTC (calendar dates), not with timezone info that GORM converts to UTC - Update TestGetOverdueTasks_Timezone_Tokyo, NewYork, InternationalDateLine - Update TestGetDueSoonTasks_Timezone_DST - Add TestIntegration_TimezoneDivergence: proves same task appears in different kanban columns based on X-Timezone header - Update TestIntegration_DateBoundaryEdgeCases to use America/New_York - Update TestIntegration_TasksByResidenceKanban to use America/Los_Angeles - Add identity-based column membership assertions (columnTaskIDs approach) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -104,13 +104,18 @@ func ScopeNotInProgress(db *gorm.DB) *gorm.DB {
|
||||
//
|
||||
// Predicate equivalent: IsOverdue(task, now)
|
||||
//
|
||||
// SQL: COALESCE(next_due_date, due_date) < ? AND active AND not_completed
|
||||
// IMPORTANT: Due dates are stored as midnight UTC but represent calendar dates.
|
||||
// We compare dates (YYYY-MM-DD), not timestamps, to ensure timezone correctness.
|
||||
// A task due "2024-12-16" should only be overdue when the user's date is "2024-12-17" or later.
|
||||
//
|
||||
// Uses DATE() function which works in both PostgreSQL and SQLite.
|
||||
func ScopeOverdue(now time.Time) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
// Compute start of day in Go for database-agnostic comparison
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
// Extract user's current date as a string for date-only comparison
|
||||
// This ensures timezone correctness: the user's "today" determines overdue status
|
||||
todayStr := now.Format("2006-01-02")
|
||||
return db.Scopes(ScopeActive, ScopeNotCompleted).
|
||||
Where("COALESCE(next_due_date, due_date) < ?", startOfDay)
|
||||
Where("DATE(COALESCE(next_due_date, due_date)) < DATE(?)", todayStr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,17 +128,16 @@ func ScopeOverdue(now time.Time) func(db *gorm.DB) *gorm.DB {
|
||||
//
|
||||
// Predicate equivalent: IsDueSoon(task, now, daysThreshold)
|
||||
//
|
||||
// SQL: COALESCE(next_due_date, due_date) >= ? AND COALESCE(next_due_date, due_date) < ?
|
||||
//
|
||||
// AND active AND not_completed
|
||||
// IMPORTANT: Uses date comparison (not timestamps) for timezone correctness.
|
||||
func ScopeDueSoon(now time.Time, daysThreshold int) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
// Compute start of day and threshold in Go for database-agnostic comparison
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
threshold := startOfDay.AddDate(0, 0, daysThreshold)
|
||||
// Extract dates as strings for date-only comparison
|
||||
todayStr := now.Format("2006-01-02")
|
||||
thresholdDate := now.AddDate(0, 0, daysThreshold)
|
||||
thresholdStr := thresholdDate.Format("2006-01-02")
|
||||
return db.Scopes(ScopeActive, ScopeNotCompleted).
|
||||
Where("COALESCE(next_due_date, due_date) >= ?", startOfDay).
|
||||
Where("COALESCE(next_due_date, due_date) < ?", threshold)
|
||||
Where("DATE(COALESCE(next_due_date, due_date)) >= DATE(?)", todayStr).
|
||||
Where("DATE(COALESCE(next_due_date, due_date)) < DATE(?)", thresholdStr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,34 +146,32 @@ func ScopeDueSoon(now time.Time, daysThreshold int) func(db *gorm.DB) *gorm.DB {
|
||||
// A task is "upcoming" when its effective date is >= start of (today + threshold) OR is null,
|
||||
// and it's active and not completed.
|
||||
//
|
||||
// Note: Uses start of day for comparisons for consistency with other scopes.
|
||||
//
|
||||
// Predicate equivalent: IsUpcoming(task, now, daysThreshold)
|
||||
//
|
||||
// SQL: (COALESCE(next_due_date, due_date) >= ? OR (next_due_date IS NULL AND due_date IS NULL))
|
||||
//
|
||||
// AND active AND not_completed
|
||||
// IMPORTANT: Uses date comparison (not timestamps) for timezone correctness.
|
||||
func ScopeUpcoming(now time.Time, daysThreshold int) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
// Compute threshold as start of day + N days in Go for database-agnostic comparison
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
threshold := startOfDay.AddDate(0, 0, daysThreshold)
|
||||
// Compute threshold date
|
||||
thresholdDate := now.AddDate(0, 0, daysThreshold)
|
||||
thresholdStr := thresholdDate.Format("2006-01-02")
|
||||
return db.Scopes(ScopeActive, ScopeNotCompleted).
|
||||
Where(
|
||||
"COALESCE(next_due_date, due_date) >= ? OR (next_due_date IS NULL AND due_date IS NULL)",
|
||||
threshold,
|
||||
"DATE(COALESCE(next_due_date, due_date)) >= DATE(?) OR (next_due_date IS NULL AND due_date IS NULL)",
|
||||
thresholdStr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeDueInRange returns a scope for tasks with effective date in a range.
|
||||
//
|
||||
// SQL: COALESCE(next_due_date, due_date) >= ? AND COALESCE(next_due_date, due_date) < ?
|
||||
// IMPORTANT: Uses date comparison (not timestamps) for timezone correctness.
|
||||
func ScopeDueInRange(start, end time.Time) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
startStr := start.Format("2006-01-02")
|
||||
endStr := end.Format("2006-01-02")
|
||||
return db.
|
||||
Where("COALESCE(next_due_date, due_date) >= ?", start).
|
||||
Where("COALESCE(next_due_date, due_date) < ?", end)
|
||||
Where("DATE(COALESCE(next_due_date, due_date)) >= DATE(?)", startStr).
|
||||
Where("DATE(COALESCE(next_due_date, due_date)) < DATE(?)", endStr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user