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:
@@ -101,16 +101,16 @@ func (h *Handler) HandleTaskReminder(ctx context.Context, task *asynq.Task) erro
|
||||
log.Info().Int("eligible_users", len(eligibleUserIDs)).Msg("Found users eligible for task reminders this hour")
|
||||
|
||||
// Step 2: Query tasks due today or tomorrow only for eligible users
|
||||
// A task is considered "completed" (and should be excluded) if:
|
||||
// - NextDueDate IS NULL AND it has at least one completion record
|
||||
// This matches the kanban categorization logic
|
||||
// Completion detection logic matches internal/task/predicates.IsCompleted:
|
||||
// A task is "completed" when NextDueDate == nil AND has at least one completion.
|
||||
// See internal/task/scopes.ScopeNotCompleted for the SQL equivalent.
|
||||
var dueSoonTasks []models.Task
|
||||
err = h.db.Preload("Status").Preload("Completions").Preload("Residence").
|
||||
Where("(due_date >= ? AND due_date < ?) OR (next_due_date >= ? AND next_due_date < ?)",
|
||||
today, dayAfterTomorrow, today, dayAfterTomorrow).
|
||||
Where("is_cancelled = false").
|
||||
Where("is_archived = false").
|
||||
// Exclude completed tasks: tasks with no next_due_date AND at least one completion
|
||||
// Exclude completed tasks (matches scopes.ScopeNotCompleted)
|
||||
Where("NOT (next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = task_task.id))").
|
||||
Where("(assigned_to_id IN ? OR residence_id IN (SELECT id FROM residence_residence WHERE owner_id IN ?))",
|
||||
eligibleUserIDs, eligibleUserIDs).
|
||||
@@ -218,15 +218,15 @@ func (h *Handler) HandleOverdueReminder(ctx context.Context, task *asynq.Task) e
|
||||
log.Info().Int("eligible_users", len(eligibleUserIDs)).Msg("Found users eligible for overdue reminders this hour")
|
||||
|
||||
// Step 2: Query overdue tasks only for eligible users
|
||||
// A task is considered "completed" (and should be excluded) if:
|
||||
// - NextDueDate IS NULL AND it has at least one completion record
|
||||
// This matches the kanban categorization logic
|
||||
// Completion detection logic matches internal/task/predicates.IsCompleted:
|
||||
// A task is "completed" when NextDueDate == nil AND has at least one completion.
|
||||
// See internal/task/scopes.ScopeNotCompleted for the SQL equivalent.
|
||||
var overdueTasks []models.Task
|
||||
err = h.db.Preload("Status").Preload("Completions").Preload("Residence").
|
||||
Where("due_date < ? OR next_due_date < ?", today, today).
|
||||
Where("is_cancelled = false").
|
||||
Where("is_archived = false").
|
||||
// Exclude completed tasks: tasks with no next_due_date AND at least one completion
|
||||
// Exclude completed tasks (matches scopes.ScopeNotCompleted)
|
||||
Where("NOT (next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = task_task.id))").
|
||||
Where("(assigned_to_id IN ? OR residence_id IN (SELECT id FROM residence_residence WHERE owner_id IN ?))",
|
||||
eligibleUserIDs, eligibleUserIDs).
|
||||
@@ -299,6 +299,9 @@ func (h *Handler) HandleDailyDigest(ctx context.Context, task *asynq.Task) error
|
||||
nextWeek := today.AddDate(0, 0, 7)
|
||||
|
||||
// Get all users with their task statistics
|
||||
// Completion detection logic matches internal/task/predicates.IsCompleted:
|
||||
// A task is "completed" when NextDueDate == nil AND has at least one completion.
|
||||
// We use COALESCE(next_due_date, due_date) as the effective date for categorization.
|
||||
var userStats []struct {
|
||||
UserID uint
|
||||
TotalTasks int
|
||||
@@ -310,8 +313,16 @@ func (h *Handler) HandleDailyDigest(ctx context.Context, task *asynq.Task) error
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
COUNT(DISTINCT t.id) as total_tasks,
|
||||
COUNT(DISTINCT CASE WHEN t.due_date < ? AND tc.id IS NULL THEN t.id END) as overdue_tasks,
|
||||
COUNT(DISTINCT CASE WHEN t.due_date >= ? AND t.due_date < ? AND tc.id IS NULL THEN t.id END) as due_this_week
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN COALESCE(t.next_due_date, t.due_date) < ?
|
||||
AND NOT (t.next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = t.id))
|
||||
THEN t.id
|
||||
END) as overdue_tasks,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN COALESCE(t.next_due_date, t.due_date) >= ? AND COALESCE(t.next_due_date, t.due_date) < ?
|
||||
AND NOT (t.next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = t.id))
|
||||
THEN t.id
|
||||
END) as due_this_week
|
||||
FROM auth_user u
|
||||
JOIN residence_residence r ON r.owner_id = u.id OR r.id IN (
|
||||
SELECT residence_id FROM residence_residence_users WHERE user_id = u.id
|
||||
@@ -319,7 +330,6 @@ func (h *Handler) HandleDailyDigest(ctx context.Context, task *asynq.Task) error
|
||||
JOIN task_task t ON t.residence_id = r.id
|
||||
AND t.is_cancelled = false
|
||||
AND t.is_archived = false
|
||||
LEFT JOIN task_taskcompletion tc ON t.id = tc.task_id
|
||||
WHERE u.is_active = true
|
||||
GROUP BY u.id
|
||||
HAVING COUNT(DISTINCT t.id) > 0
|
||||
|
||||
Reference in New Issue
Block a user