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:
189
internal/task/predicates/predicates.go
Normal file
189
internal/task/predicates/predicates.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Package predicates provides pure predicate functions for task logic.
|
||||
// These functions are the SINGLE SOURCE OF TRUTH for all task-related business logic.
|
||||
//
|
||||
// IMPORTANT: The scopes in ../scopes/scopes.go must mirror these predicates exactly.
|
||||
// Any change to predicate logic MUST be reflected in the corresponding scope.
|
||||
// Tests verify consistency between predicates and scopes.
|
||||
package predicates
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// STATE PREDICATES
|
||||
// =============================================================================
|
||||
|
||||
// IsCompleted returns true if a task is considered "completed" per kanban rules.
|
||||
//
|
||||
// A task is completed when:
|
||||
// - NextDueDate is nil (no future occurrence scheduled)
|
||||
// - AND it has at least one completion record
|
||||
//
|
||||
// This applies to one-time tasks. Recurring tasks always have a NextDueDate
|
||||
// after completion, so they never enter the "completed" state permanently.
|
||||
//
|
||||
// SQL equivalent (in scopes.go ScopeCompleted):
|
||||
//
|
||||
// next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = task_task.id)
|
||||
func IsCompleted(task *models.Task) bool {
|
||||
return task.NextDueDate == nil && len(task.Completions) > 0
|
||||
}
|
||||
|
||||
// IsActive returns true if the task is not cancelled and not archived.
|
||||
// Active tasks are eligible for display in the kanban board.
|
||||
//
|
||||
// SQL equivalent (in scopes.go ScopeActive):
|
||||
//
|
||||
// is_cancelled = false AND is_archived = false
|
||||
func IsActive(task *models.Task) bool {
|
||||
return !task.IsCancelled && !task.IsArchived
|
||||
}
|
||||
|
||||
// IsCancelled returns true if the task has been cancelled.
|
||||
//
|
||||
// SQL equivalent:
|
||||
//
|
||||
// is_cancelled = true
|
||||
func IsCancelled(task *models.Task) bool {
|
||||
return task.IsCancelled
|
||||
}
|
||||
|
||||
// IsArchived returns true if the task has been archived.
|
||||
//
|
||||
// SQL equivalent:
|
||||
//
|
||||
// is_archived = true
|
||||
func IsArchived(task *models.Task) bool {
|
||||
return task.IsArchived
|
||||
}
|
||||
|
||||
// IsInProgress returns true if the task has status "In Progress".
|
||||
//
|
||||
// SQL equivalent (in scopes.go ScopeInProgress):
|
||||
//
|
||||
// task_taskstatus.name = 'In Progress'
|
||||
func IsInProgress(task *models.Task) bool {
|
||||
return task.Status != nil && task.Status.Name == "In Progress"
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DATE PREDICATES
|
||||
// =============================================================================
|
||||
|
||||
// EffectiveDate returns the date used for scheduling calculations.
|
||||
//
|
||||
// For recurring tasks that have been completed at least once, NextDueDate
|
||||
// contains the next occurrence. For new tasks or one-time tasks, we fall
|
||||
// back to DueDate.
|
||||
//
|
||||
// Returns nil if task has no due date set.
|
||||
//
|
||||
// SQL equivalent:
|
||||
//
|
||||
// COALESCE(next_due_date, due_date)
|
||||
func EffectiveDate(task *models.Task) *time.Time {
|
||||
if task.NextDueDate != nil {
|
||||
return task.NextDueDate
|
||||
}
|
||||
return task.DueDate
|
||||
}
|
||||
|
||||
// IsOverdue returns true if the task's effective date is in the past.
|
||||
//
|
||||
// A task is overdue when:
|
||||
// - It has an effective date (NextDueDate or DueDate)
|
||||
// - That date is before the given time
|
||||
// - The task is not completed, cancelled, or archived
|
||||
//
|
||||
// SQL equivalent (in scopes.go ScopeOverdue):
|
||||
//
|
||||
// COALESCE(next_due_date, due_date) < ?
|
||||
// AND NOT (next_due_date IS NULL AND EXISTS completion)
|
||||
// AND is_cancelled = false AND is_archived = false
|
||||
func IsOverdue(task *models.Task, now time.Time) bool {
|
||||
if !IsActive(task) || IsCompleted(task) {
|
||||
return false
|
||||
}
|
||||
effectiveDate := EffectiveDate(task)
|
||||
if effectiveDate == nil {
|
||||
return false
|
||||
}
|
||||
return effectiveDate.Before(now)
|
||||
}
|
||||
|
||||
// IsDueSoon returns true if the task's effective date is within the threshold.
|
||||
//
|
||||
// A task is "due soon" when:
|
||||
// - It has an effective date (NextDueDate or DueDate)
|
||||
// - That date is >= now AND < (now + daysThreshold)
|
||||
// - The task is not completed, cancelled, archived, or already overdue
|
||||
//
|
||||
// SQL equivalent (in scopes.go ScopeDueSoon):
|
||||
//
|
||||
// COALESCE(next_due_date, due_date) >= ?
|
||||
// AND COALESCE(next_due_date, due_date) < ?
|
||||
// AND NOT (next_due_date IS NULL AND EXISTS completion)
|
||||
// AND is_cancelled = false AND is_archived = false
|
||||
func IsDueSoon(task *models.Task, now time.Time, daysThreshold int) bool {
|
||||
if !IsActive(task) || IsCompleted(task) {
|
||||
return false
|
||||
}
|
||||
effectiveDate := EffectiveDate(task)
|
||||
if effectiveDate == nil {
|
||||
return false
|
||||
}
|
||||
threshold := now.AddDate(0, 0, daysThreshold)
|
||||
// Due soon = not overdue AND before threshold
|
||||
return !effectiveDate.Before(now) && effectiveDate.Before(threshold)
|
||||
}
|
||||
|
||||
// IsUpcoming returns true if the task is due after the threshold or has no due date.
|
||||
//
|
||||
// A task is "upcoming" when:
|
||||
// - It has no effective date, OR
|
||||
// - Its effective date is >= (now + daysThreshold)
|
||||
// - The task is not completed, cancelled, or archived
|
||||
//
|
||||
// This is the default category for tasks that don't match other criteria.
|
||||
func IsUpcoming(task *models.Task, now time.Time, daysThreshold int) bool {
|
||||
if !IsActive(task) || IsCompleted(task) {
|
||||
return false
|
||||
}
|
||||
effectiveDate := EffectiveDate(task)
|
||||
if effectiveDate == nil {
|
||||
return true // No due date = upcoming
|
||||
}
|
||||
threshold := now.AddDate(0, 0, daysThreshold)
|
||||
return !effectiveDate.Before(threshold)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPLETION HELPERS
|
||||
// =============================================================================
|
||||
|
||||
// HasCompletions returns true if the task has at least one completion record.
|
||||
func HasCompletions(task *models.Task) bool {
|
||||
return len(task.Completions) > 0
|
||||
}
|
||||
|
||||
// CompletionCount returns the number of completions for a task.
|
||||
func CompletionCount(task *models.Task) int {
|
||||
return len(task.Completions)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RECURRING TASK HELPERS
|
||||
// =============================================================================
|
||||
|
||||
// IsRecurring returns true if the task has a recurring frequency.
|
||||
func IsRecurring(task *models.Task) bool {
|
||||
return task.Frequency != nil && task.Frequency.Days != nil && *task.Frequency.Days > 0
|
||||
}
|
||||
|
||||
// IsOneTime returns true if the task is a one-time (non-recurring) task.
|
||||
func IsOneTime(task *models.Task) bool {
|
||||
return !IsRecurring(task)
|
||||
}
|
||||
522
internal/task/predicates/predicates_test.go
Normal file
522
internal/task/predicates/predicates_test.go
Normal file
@@ -0,0 +1,522 @@
|
||||
package predicates_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/task/predicates"
|
||||
)
|
||||
|
||||
// Helper to create a time pointer
|
||||
func timePtr(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
func TestIsCompleted(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "completed: NextDueDate nil with completions",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not completed: NextDueDate set with completions",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(time.Now().AddDate(0, 0, 7)),
|
||||
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not completed: NextDueDate nil without completions",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not completed: NextDueDate set without completions",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(time.Now().AddDate(0, 0, 7)),
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.IsCompleted(tt.task)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsCompleted() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsActive(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "active: not cancelled, not archived",
|
||||
task: &models.Task{IsCancelled: false, IsArchived: false},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not active: cancelled",
|
||||
task: &models.Task{IsCancelled: true, IsArchived: false},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not active: archived",
|
||||
task: &models.Task{IsCancelled: false, IsArchived: true},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not active: both cancelled and archived",
|
||||
task: &models.Task{IsCancelled: true, IsArchived: true},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.IsActive(tt.task)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsActive() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInProgress(t *testing.T) {
|
||||
inProgressStatus := &models.TaskStatus{Name: "In Progress"}
|
||||
pendingStatus := &models.TaskStatus{Name: "Pending"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "in progress: status is In Progress",
|
||||
task: &models.Task{Status: inProgressStatus},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not in progress: status is Pending",
|
||||
task: &models.Task{Status: pendingStatus},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not in progress: no status",
|
||||
task: &models.Task{Status: nil},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.IsInProgress(tt.task)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsInProgress() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveDate(t *testing.T) {
|
||||
now := time.Now()
|
||||
nextWeek := now.AddDate(0, 0, 7)
|
||||
nextMonth := now.AddDate(0, 1, 0)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
expected *time.Time
|
||||
}{
|
||||
{
|
||||
name: "prefers NextDueDate when both set",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(nextWeek),
|
||||
DueDate: timePtr(nextMonth),
|
||||
},
|
||||
expected: timePtr(nextWeek),
|
||||
},
|
||||
{
|
||||
name: "falls back to DueDate when NextDueDate nil",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
DueDate: timePtr(nextMonth),
|
||||
},
|
||||
expected: timePtr(nextMonth),
|
||||
},
|
||||
{
|
||||
name: "returns nil when both nil",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
DueDate: nil,
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.EffectiveDate(tt.task)
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
t.Errorf("EffectiveDate() = %v, expected nil", result)
|
||||
}
|
||||
} else {
|
||||
if result == nil {
|
||||
t.Errorf("EffectiveDate() = nil, expected %v", tt.expected)
|
||||
} else if !result.Equal(*tt.expected) {
|
||||
t.Errorf("EffectiveDate() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOverdue(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
tomorrow := now.AddDate(0, 0, 1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
now time.Time
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "overdue: effective date in past",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(yesterday),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not overdue: effective date in future",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(tomorrow),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not overdue: cancelled task",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(yesterday),
|
||||
IsCancelled: true,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not overdue: archived task",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(yesterday),
|
||||
IsCancelled: false,
|
||||
IsArchived: true,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not overdue: completed task (NextDueDate nil with completions)",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
DueDate: timePtr(yesterday),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
|
||||
},
|
||||
now: now,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not overdue: no due date",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
DueDate: nil,
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "overdue: uses DueDate when NextDueDate nil (no completions)",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
DueDate: timePtr(yesterday),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.IsOverdue(tt.task, tt.now)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsOverdue() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDueSoon(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
in5Days := now.AddDate(0, 0, 5)
|
||||
in60Days := now.AddDate(0, 0, 60)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
now time.Time
|
||||
daysThreshold int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "due soon: within threshold",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(in5Days),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not due soon: beyond threshold",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(in60Days),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not due soon: overdue (in past)",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(yesterday),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not due soon: cancelled",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(in5Days),
|
||||
IsCancelled: true,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not due soon: completed",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
DueDate: timePtr(in5Days),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.IsDueSoon(tt.task, tt.now, tt.daysThreshold)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsDueSoon() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUpcoming(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
in5Days := now.AddDate(0, 0, 5)
|
||||
in60Days := now.AddDate(0, 0, 60)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
now time.Time
|
||||
daysThreshold int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "upcoming: beyond threshold",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(in60Days),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "upcoming: no due date",
|
||||
task: &models.Task{
|
||||
NextDueDate: nil,
|
||||
DueDate: nil,
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not upcoming: within due soon threshold",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(in5Days),
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not upcoming: cancelled",
|
||||
task: &models.Task{
|
||||
NextDueDate: timePtr(in60Days),
|
||||
IsCancelled: true,
|
||||
IsArchived: false,
|
||||
Completions: []models.TaskCompletion{},
|
||||
},
|
||||
now: now,
|
||||
daysThreshold: 30,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.IsUpcoming(tt.task, tt.now, tt.daysThreshold)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsUpcoming() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasCompletions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "has completions",
|
||||
task: &models.Task{Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no completions",
|
||||
task: &models.Task{Completions: []models.TaskCompletion{}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil completions",
|
||||
task: &models.Task{Completions: nil},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.HasCompletions(tt.task)
|
||||
if result != tt.expected {
|
||||
t.Errorf("HasCompletions() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRecurring(t *testing.T) {
|
||||
days := 7
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task *models.Task
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "recurring: frequency with days",
|
||||
task: &models.Task{Frequency: &models.TaskFrequency{Days: &days}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not recurring: frequency without days (one-time)",
|
||||
task: &models.Task{Frequency: &models.TaskFrequency{Days: nil}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not recurring: no frequency",
|
||||
task: &models.Task{Frequency: nil},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := predicates.IsRecurring(tt.task)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsRecurring() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user