Migrate from Gin to Echo framework and add comprehensive integration tests

Major changes:
- Migrate all handlers from Gin to Echo framework
- Add new apperrors, echohelpers, and validator packages
- Update middleware for Echo compatibility
- Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column)
- Add 6 new integration tests:
  - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks
  - MultiUserSharing: Complex sharing with user removal
  - TaskStateTransitions: All state transitions and kanban column changes
  - DateBoundaryEdgeCases: Threshold boundary testing
  - CascadeOperations: Residence deletion cascade effects
  - MultiUserOperations: Shared residence collaboration
- Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.)
- Fix RemoveUser route param mismatch (userId -> user_id)
- Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-16 13:52:08 -06:00
parent c51f1ce34a
commit 6dac34e373
98 changed files with 8209 additions and 4425 deletions

View File

@@ -98,66 +98,65 @@ func ScopeNotInProgress(db *gorm.DB) *gorm.DB {
// ScopeOverdue returns a scope for overdue tasks.
//
// A task is overdue when its effective date (COALESCE(next_due_date, due_date))
// is before the given time, and it's active and not completed.
// is before the start of the given day, and it's active and not completed.
//
// Note: A task due "today" is NOT overdue. It becomes overdue tomorrow.
//
// Predicate equivalent: IsOverdue(task, now)
//
// SQL: COALESCE(next_due_date, due_date) < ?::timestamp AND active AND not_completed
//
// NOTE: We explicitly cast to timestamp because PostgreSQL DATE columns compared
// against string literals (which is how GORM passes time.Time) use date comparison,
// not timestamp comparison. For example:
// - '2025-12-07'::date < '2025-12-07 17:00:00' = false (compares dates only)
// - '2025-12-07'::date < '2025-12-07 17:00:00'::timestamp = true (compares timestamp)
// SQL: COALESCE(next_due_date, due_date) < ? AND active AND not_completed
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())
return db.Scopes(ScopeActive, ScopeNotCompleted).
Where("COALESCE(next_due_date, due_date)::timestamp < ?::timestamp", now)
Where("COALESCE(next_due_date, due_date) < ?", startOfDay)
}
}
// ScopeDueSoon returns a scope for tasks due within the threshold.
//
// A task is "due soon" when its effective date is >= now AND < (now + threshold),
// A task is "due soon" when its effective date is >= start of today AND < start of (today + threshold),
// and it's active and not completed.
//
// Note: Uses day-level comparisons so tasks due "today" are included.
//
// Predicate equivalent: IsDueSoon(task, now, daysThreshold)
//
// SQL: COALESCE(next_due_date, due_date)::timestamp >= ?::timestamp
// SQL: COALESCE(next_due_date, due_date) >= ? AND COALESCE(next_due_date, due_date) < ?
//
// AND COALESCE(next_due_date, due_date)::timestamp < ?::timestamp
// AND active AND not_completed
//
// NOTE: We explicitly cast to timestamp for consistent comparison with DATE columns.
// See ScopeOverdue for detailed explanation.
func ScopeDueSoon(now time.Time, daysThreshold int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
threshold := now.AddDate(0, 0, daysThreshold)
// 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)
return db.Scopes(ScopeActive, ScopeNotCompleted).
Where("COALESCE(next_due_date, due_date)::timestamp >= ?::timestamp", now).
Where("COALESCE(next_due_date, due_date)::timestamp < ?::timestamp", threshold)
Where("COALESCE(next_due_date, due_date) >= ?", startOfDay).
Where("COALESCE(next_due_date, due_date) < ?", threshold)
}
}
// ScopeUpcoming returns a scope for tasks due after the threshold or with no due date.
//
// A task is "upcoming" when its effective date is >= (now + threshold) OR is null,
// 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)::timestamp >= ?::timestamp OR (next_due_date IS NULL AND due_date IS NULL))
// SQL: (COALESCE(next_due_date, due_date) >= ? OR (next_due_date IS NULL AND due_date IS NULL))
//
// AND active AND not_completed
//
// NOTE: We explicitly cast to timestamp for consistent comparison with DATE columns.
// See ScopeOverdue for detailed explanation.
func ScopeUpcoming(now time.Time, daysThreshold int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
threshold := now.AddDate(0, 0, daysThreshold)
// 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)
return db.Scopes(ScopeActive, ScopeNotCompleted).
Where(
"COALESCE(next_due_date, due_date)::timestamp >= ?::timestamp OR (next_due_date IS NULL AND due_date IS NULL)",
"COALESCE(next_due_date, due_date) >= ? OR (next_due_date IS NULL AND due_date IS NULL)",
threshold,
)
}
@@ -165,17 +164,12 @@ func ScopeUpcoming(now time.Time, daysThreshold int) func(db *gorm.DB) *gorm.DB
// ScopeDueInRange returns a scope for tasks with effective date in a range.
//
// SQL: COALESCE(next_due_date, due_date)::timestamp >= ?::timestamp
//
// AND COALESCE(next_due_date, due_date)::timestamp < ?::timestamp
//
// NOTE: We explicitly cast to timestamp for consistent comparison with DATE columns.
// See ScopeOverdue for detailed explanation.
// SQL: COALESCE(next_due_date, due_date) >= ? AND COALESCE(next_due_date, due_date) < ?
func ScopeDueInRange(start, end time.Time) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.
Where("COALESCE(next_due_date, due_date)::timestamp >= ?::timestamp", start).
Where("COALESCE(next_due_date, due_date)::timestamp < ?::timestamp", end)
Where("COALESCE(next_due_date, due_date) >= ?", start).
Where("COALESCE(next_due_date, due_date) < ?", end)
}
}