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

@@ -7,6 +7,7 @@ import (
"github.com/shopspring/decimal"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/task/categorization"
)
// TaskCategoryResponse represents a task category
@@ -365,53 +366,8 @@ func NewTaskCompletionWithTaskResponse(c *models.TaskCompletion, task *models.Ta
}
// DetermineKanbanColumn determines which kanban column a task belongs to.
// This is a wrapper around the Chain of Responsibility implementation in
// internal/task/categorization package. See that package for detailed
// documentation on the categorization logic.
//
// Deprecated: Use categorization.DetermineKanbanColumn directly for new code.
// Delegates to internal/task/categorization package which is the single source
// of truth for task categorization logic.
func DetermineKanbanColumn(task *models.Task, daysThreshold int) string {
// Import would cause circular dependency, so we replicate the logic here
// for backwards compatibility. The authoritative implementation is in
// internal/task/categorization/chain.go
if daysThreshold <= 0 {
daysThreshold = 30 // Default
}
now := time.Now().UTC()
threshold := now.AddDate(0, 0, daysThreshold)
// Priority order (Chain of Responsibility):
// 1. Cancelled (highest priority)
if task.IsCancelled {
return "cancelled_tasks"
}
// 2. Completed (one-time task with nil next_due_date and has completions)
if task.NextDueDate == nil && len(task.Completions) > 0 {
return "completed_tasks"
}
// 3. In Progress (status check)
if task.Status != nil && task.Status.Name == "In Progress" {
return "in_progress_tasks"
}
// 4. Overdue (next_due_date or due_date is in the past)
effectiveDate := task.NextDueDate
if effectiveDate == nil {
effectiveDate = task.DueDate
}
if effectiveDate != nil {
if effectiveDate.Before(now) {
return "overdue_tasks"
}
// 5. Due Soon (within threshold)
if effectiveDate.Before(threshold) {
return "due_soon_tasks"
}
}
// 6. Upcoming (default/fallback)
return "upcoming_tasks"
return categorization.DetermineKanbanColumn(task, daysThreshold)
}