Consolidate task logic into single source of truth (DRY refactor)

This refactor eliminates duplicate task logic across the codebase by
creating a centralized task package with three layers:

- predicates/: Pure Go functions defining task state logic (IsCompleted,
  IsOverdue, IsDueSoon, IsUpcoming, IsActive, IsInProgress, EffectiveDate)
- scopes/: GORM scope functions mirroring predicates for database queries
- categorization/: Chain of Responsibility pattern for kanban column assignment

Key fixes:
- Fixed PostgreSQL DATE vs TIMESTAMP comparison bug in scopes (added
  explicit ::timestamp casts) that caused summary/kanban count mismatches
- Fixed models/task.go IsOverdue() and IsDueSoon() to use EffectiveDate
  (NextDueDate ?? DueDate) instead of only DueDate
- Removed duplicate isTaskCompleted() helpers from task_repo.go and
  task_button_types.go

Files refactored to use consolidated logic:
- task_repo.go: Uses scopes for statistics, predicates for filtering
- task_button_types.go: Uses predicates instead of inline logic
- responses/task.go: Delegates to categorization package
- dashboard_handler.go: Uses scopes for task statistics
- residence_service.go: Uses predicates for report generation
- worker/jobs/handler.go: Documented SQL with predicate references

Added comprehensive tests:
- predicates_test.go: Unit tests for all predicate functions
- scopes_test.go: Integration tests verifying scopes match predicates
- consistency_test.go: Three-layer consistency tests ensuring predicates,
  scopes, and categorization all return identical results

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-07 11:48:03 -06:00
parent f0c7b070d7
commit cfb8a28870
16 changed files with 3408 additions and 679 deletions

View File

@@ -110,28 +110,66 @@ func (Task) TableName() string {
return "task_task"
}
// IsOverdue returns true if the task is past its due date and not completed
// IsOverdue returns true if the task is past its due date and not completed.
//
// IMPORTANT: This method delegates to the predicates package which is the
// single source of truth for task logic. It uses EffectiveDate (NextDueDate ?? DueDate)
// rather than just DueDate, ensuring consistency with kanban categorization.
//
// Deprecated: Prefer using task.IsOverdue(t, time.Now().UTC()) directly for explicit time control.
func (t *Task) IsOverdue() bool {
if t.DueDate == nil || t.IsCancelled || t.IsArchived {
// Delegate to predicates package - single source of truth
// Import is avoided here to prevent circular dependency.
// Logic must match predicates.IsOverdue exactly:
// - Check active (not cancelled, not archived)
// - Check not completed (NextDueDate != nil || no completions)
// - Check effective date < now
if t.IsCancelled || t.IsArchived {
return false
}
// Check if there's a completion
if len(t.Completions) > 0 {
// Completed check: NextDueDate == nil AND has completions
if t.NextDueDate == nil && len(t.Completions) > 0 {
return false
}
return time.Now().UTC().After(*t.DueDate)
// Effective date: NextDueDate ?? DueDate
effectiveDate := t.NextDueDate
if effectiveDate == nil {
effectiveDate = t.DueDate
}
if effectiveDate == nil {
return false
}
return effectiveDate.Before(time.Now().UTC())
}
// IsDueSoon returns true if the task is due within the specified days
// IsDueSoon returns true if the task is due within the specified days.
//
// IMPORTANT: This method uses EffectiveDate (NextDueDate ?? DueDate)
// rather than just DueDate, ensuring consistency with kanban categorization.
//
// Deprecated: Prefer using task.IsDueSoon(t, time.Now().UTC(), days) directly for explicit time control.
func (t *Task) IsDueSoon(days int) bool {
if t.DueDate == nil || t.IsCancelled || t.IsArchived {
// Delegate to predicates package logic - single source of truth
// Logic must match predicates.IsDueSoon exactly
if t.IsCancelled || t.IsArchived {
return false
}
if len(t.Completions) > 0 {
// Completed check: NextDueDate == nil AND has completions
if t.NextDueDate == nil && len(t.Completions) > 0 {
return false
}
threshold := time.Now().UTC().AddDate(0, 0, days)
return t.DueDate.Before(threshold) && !t.IsOverdue()
// Effective date: NextDueDate ?? DueDate
effectiveDate := t.NextDueDate
if effectiveDate == nil {
effectiveDate = t.DueDate
}
if effectiveDate == nil {
return false
}
now := time.Now().UTC()
threshold := now.AddDate(0, 0, days)
// Due soon = not overdue AND before threshold
return !effectiveDate.Before(now) && effectiveDate.Before(threshold)
}
// TaskCompletion represents the task_taskcompletion table