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:
@@ -110,10 +110,11 @@ func (r *TaskRepository) GetOverdueTasks(now time.Time, opts TaskFilterOptions)
|
||||
if opts.IncludeArchived {
|
||||
// When including archived, build the query manually to skip the archived check
|
||||
// but still apply cancelled check, not-completed check, and date check
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
// IMPORTANT: Use date comparison (not timestamps) for timezone correctness
|
||||
todayStr := now.Format("2006-01-02")
|
||||
query = query.Where("is_cancelled = ?", false).
|
||||
Scopes(task.ScopeNotCompleted).
|
||||
Where("COALESCE(next_due_date, due_date) < ?", startOfDay)
|
||||
Where("DATE(COALESCE(next_due_date, due_date)) < DATE(?)", todayStr)
|
||||
} else {
|
||||
// Use the combined scope which includes is_archived = false
|
||||
query = query.Scopes(task.ScopeOverdue(now))
|
||||
|
||||
@@ -1493,9 +1493,10 @@ func TestGetOverdueTasks_Timezone_Tokyo(t *testing.T) {
|
||||
|
||||
tokyo, _ := time.LoadLocation("Asia/Tokyo")
|
||||
|
||||
// Task due on Dec 15 at midnight in Tokyo timezone
|
||||
// When stored as UTC, this becomes Dec 14 15:00 UTC
|
||||
dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, tokyo)
|
||||
// IMPORTANT: Due dates are stored as midnight UTC representing calendar dates.
|
||||
// A task "due on Dec 15" is stored as 2025-12-15 00:00:00 UTC.
|
||||
// Timezone handling happens at query time via the `now` parameter.
|
||||
dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, time.UTC)
|
||||
task := &models.Task{
|
||||
ResidenceID: residence.ID,
|
||||
CreatedByID: user.ID,
|
||||
@@ -1506,13 +1507,14 @@ func TestGetOverdueTasks_Timezone_Tokyo(t *testing.T) {
|
||||
|
||||
opts := TaskFilterOptions{ResidenceID: residence.ID}
|
||||
|
||||
// When it's Dec 15 23:00 in Tokyo, task should NOT be overdue (still Dec 15)
|
||||
// When it's Dec 15 23:00 in Tokyo, task should NOT be overdue (still Dec 15 in Tokyo)
|
||||
// The `now` parameter's date (Dec 15) is compared against the due date (Dec 15)
|
||||
tokyoDec15Evening := time.Date(2025, 12, 15, 23, 0, 0, 0, tokyo)
|
||||
tasks, err := repo.GetOverdueTasks(tokyoDec15Evening, opts)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tasks, 0, "Task should NOT be overdue on Dec 15 in Tokyo timezone")
|
||||
|
||||
// When it's Dec 16 00:00 in Tokyo, task IS overdue (now Dec 16)
|
||||
// When it's Dec 16 00:00 in Tokyo, task IS overdue (now Dec 16 in Tokyo, due Dec 15)
|
||||
tokyoDec16Midnight := time.Date(2025, 12, 16, 0, 0, 0, 0, tokyo)
|
||||
tasks, err = repo.GetOverdueTasks(tokyoDec16Midnight, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -1528,9 +1530,10 @@ func TestGetOverdueTasks_Timezone_NewYork(t *testing.T) {
|
||||
|
||||
newYork, _ := time.LoadLocation("America/New_York")
|
||||
|
||||
// Task due on Dec 15 at midnight in New York timezone
|
||||
// When stored as UTC, this becomes Dec 15 05:00 UTC (EST is UTC-5)
|
||||
dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, newYork)
|
||||
// IMPORTANT: Due dates are stored as midnight UTC representing calendar dates.
|
||||
// A task "due on Dec 15" is stored as 2025-12-15 00:00:00 UTC.
|
||||
// Timezone handling happens at query time via the `now` parameter.
|
||||
dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, time.UTC)
|
||||
task := &models.Task{
|
||||
ResidenceID: residence.ID,
|
||||
CreatedByID: user.ID,
|
||||
@@ -1541,13 +1544,13 @@ func TestGetOverdueTasks_Timezone_NewYork(t *testing.T) {
|
||||
|
||||
opts := TaskFilterOptions{ResidenceID: residence.ID}
|
||||
|
||||
// When it's Dec 15 23:00 in New York, task should NOT be overdue
|
||||
// When it's Dec 15 23:00 in New York, task should NOT be overdue (still Dec 15 in NY)
|
||||
nyDec15Evening := time.Date(2025, 12, 15, 23, 0, 0, 0, newYork)
|
||||
tasks, err := repo.GetOverdueTasks(nyDec15Evening, opts)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tasks, 0, "Task should NOT be overdue on Dec 15 in New York timezone")
|
||||
|
||||
// When it's Dec 16 00:00 in New York, task IS overdue
|
||||
// When it's Dec 16 00:00 in New York, task IS overdue (now Dec 16 in NY, due Dec 15)
|
||||
nyDec16Midnight := time.Date(2025, 12, 16, 0, 0, 0, 0, newYork)
|
||||
tasks, err = repo.GetOverdueTasks(nyDec16Midnight, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -1566,8 +1569,9 @@ func TestGetOverdueTasks_Timezone_InternationalDateLine(t *testing.T) {
|
||||
auckland, _ := time.LoadLocation("Pacific/Auckland")
|
||||
honolulu, _ := time.LoadLocation("Pacific/Honolulu")
|
||||
|
||||
// Task due on Dec 15 at midnight Auckland time
|
||||
dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, auckland)
|
||||
// IMPORTANT: Due dates are stored as midnight UTC representing calendar dates.
|
||||
// Task due on Dec 15 is stored as 2025-12-15 00:00:00 UTC.
|
||||
dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, time.UTC)
|
||||
task := &models.Task{
|
||||
ResidenceID: residence.ID,
|
||||
CreatedByID: user.ID,
|
||||
@@ -1578,14 +1582,13 @@ func TestGetOverdueTasks_Timezone_InternationalDateLine(t *testing.T) {
|
||||
|
||||
opts := TaskFilterOptions{ResidenceID: residence.ID}
|
||||
|
||||
// From Auckland's perspective on Dec 16, task is overdue
|
||||
// From Auckland's perspective on Dec 16, task is overdue (Dec 16 > Dec 15)
|
||||
aucklandDec16 := time.Date(2025, 12, 16, 0, 0, 0, 0, auckland)
|
||||
tasks, err := repo.GetOverdueTasks(aucklandDec16, opts)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tasks, 1, "Task should be overdue on Dec 16 in Auckland")
|
||||
|
||||
// From Honolulu's perspective on Dec 14 (same UTC instant as Auckland Dec 15 morning),
|
||||
// it's still before the due date, so NOT overdue
|
||||
// From Honolulu's perspective on Dec 14, task is NOT overdue (Dec 14 < Dec 15)
|
||||
honoluluDec14 := time.Date(2025, 12, 14, 5, 0, 0, 0, honolulu)
|
||||
tasks, err = repo.GetOverdueTasks(honoluluDec14, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -1601,9 +1604,10 @@ func TestGetDueSoonTasks_Timezone_DST(t *testing.T) {
|
||||
|
||||
newYork, _ := time.LoadLocation("America/New_York")
|
||||
|
||||
// IMPORTANT: Due dates are stored as midnight UTC representing calendar dates.
|
||||
// 2025 DST ends Nov 2: clocks fall back from 2:00 AM to 1:00 AM
|
||||
// Task due on Nov 5 at midnight in New York timezone
|
||||
dueDate := time.Date(2025, 11, 5, 0, 0, 0, 0, newYork)
|
||||
// Task due on Nov 5 is stored as 2025-11-05 00:00:00 UTC
|
||||
dueDate := time.Date(2025, 11, 5, 0, 0, 0, 0, time.UTC)
|
||||
task := &models.Task{
|
||||
ResidenceID: residence.ID,
|
||||
CreatedByID: user.ID,
|
||||
|
||||
Reference in New Issue
Block a user