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:
@@ -8,6 +8,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/task/scopes"
|
||||
)
|
||||
|
||||
// AdminDashboardHandler handles admin dashboard endpoints
|
||||
@@ -115,43 +116,41 @@ func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
|
||||
h.db.Model(&models.Residence{}).Where("is_active = ?", true).Count(&stats.Residences.Active)
|
||||
h.db.Model(&models.Residence{}).Where("created_at >= ?", thirtyDaysAgo).Count(&stats.Residences.New30d)
|
||||
|
||||
// Task stats
|
||||
// Task stats - uses scopes from internal/task/scopes for consistency
|
||||
h.db.Model(&models.Task{}).Count(&stats.Tasks.Total)
|
||||
h.db.Model(&models.Task{}).Where("is_cancelled = ? AND is_archived = ?", false, false).Count(&stats.Tasks.Active)
|
||||
h.db.Model(&models.Task{}).Where("is_archived = ?", true).Count(&stats.Tasks.Archived)
|
||||
h.db.Model(&models.Task{}).Where("is_cancelled = ?", true).Count(&stats.Tasks.Cancelled)
|
||||
h.db.Model(&models.Task{}).Scopes(scopes.ScopeActive).Count(&stats.Tasks.Active)
|
||||
h.db.Model(&models.Task{}).Scopes(scopes.ScopeArchived).Count(&stats.Tasks.Archived)
|
||||
h.db.Model(&models.Task{}).Scopes(scopes.ScopeCancelled).Count(&stats.Tasks.Cancelled)
|
||||
h.db.Model(&models.Task{}).Where("created_at >= ?", thirtyDaysAgo).Count(&stats.Tasks.New30d)
|
||||
|
||||
// Task counts by status (using LEFT JOIN to handle tasks with no status)
|
||||
// Note: These status counts use DB status names, not kanban categorization
|
||||
h.db.Model(&models.Task{}).
|
||||
Where("is_cancelled = ? AND is_archived = ?", false, false).
|
||||
Scopes(scopes.ScopeActive).
|
||||
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
|
||||
Where("LOWER(task_taskstatus.name) = ? OR task_taskstatus.id IS NULL", "pending").
|
||||
Count(&stats.Tasks.Pending)
|
||||
|
||||
h.db.Model(&models.Task{}).
|
||||
Where("is_cancelled = ? AND is_archived = ?", false, false).
|
||||
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
|
||||
Where("LOWER(task_taskstatus.name) = ?", "in progress").
|
||||
Scopes(scopes.ScopeActive, scopes.ScopeInProgress).
|
||||
Count(&stats.Tasks.InProgress)
|
||||
|
||||
// Completed count: uses kanban completion logic (NextDueDate == nil AND has completions)
|
||||
// See internal/task/predicates.IsCompleted for the definition
|
||||
h.db.Model(&models.Task{}).
|
||||
Where("is_cancelled = ? AND is_archived = ?", false, false).
|
||||
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
|
||||
Where("LOWER(task_taskstatus.name) = ?", "completed").
|
||||
Scopes(scopes.ScopeActive, scopes.ScopeCompleted).
|
||||
Count(&stats.Tasks.Completed)
|
||||
|
||||
h.db.Model(&models.Task{}).
|
||||
Where("is_cancelled = ? AND is_archived = ?", false, false).
|
||||
Scopes(scopes.ScopeActive).
|
||||
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
|
||||
Where("LOWER(task_taskstatus.name) = ?", "on hold").
|
||||
Count(&stats.Tasks.OnHold)
|
||||
|
||||
// Overdue: past due date, not completed, not cancelled, not archived
|
||||
// Overdue: uses consistent logic from internal/task/scopes.ScopeOverdue
|
||||
// Effective date (COALESCE(next_due_date, due_date)) < now, active, not completed
|
||||
h.db.Model(&models.Task{}).
|
||||
Where("next_due_date < ? AND is_cancelled = ? AND is_archived = ?", now, false, false).
|
||||
Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
|
||||
Where("LOWER(task_taskstatus.name) NOT IN ? OR task_taskstatus.id IS NULL", []string{"completed", "cancelled"}).
|
||||
Scopes(scopes.ScopeOverdue(now)).
|
||||
Count(&stats.Tasks.Overdue)
|
||||
|
||||
// Contractor stats
|
||||
|
||||
Reference in New Issue
Block a user