# Task Logic Architecture

This document describes the consolidated task logic architecture following DRY principles. All task-related logic (completion detection, overdue calculation, kanban categorization, statistics counting) is centralized in the `internal/task/` package.

## Architecture Overview

```
internal/task/
├── predicates/
│   └── predicates.go     # SINGLE SOURCE OF TRUTH - Pure Go predicate functions
├── scopes/
│   └── scopes.go         # SQL mirrors of predicates for GORM queries
├── categorization/
│   └── chain.go          # Chain of Responsibility for kanban column determination
└── task.go               # Re-exports for convenient single import
```

## Package Responsibilities

### predicates/ - Single Source of Truth

Pure Go functions that define task logic. These are the **canonical definitions** for all task states.

```go
import "github.com/treytartt/honeydue-api/internal/task/predicates"

// State checks
predicates.IsCompleted(task)      // NextDueDate == nil && len(Completions) > 0
predicates.IsActive(task)         // !IsCancelled && !IsArchived
predicates.IsCancelled(task)      // IsCancelled == true
predicates.IsArchived(task)       // IsArchived == true
predicates.IsInProgress(task)     // InProgress == true

// Date calculations
predicates.EffectiveDate(task)    // NextDueDate ?? DueDate
predicates.IsOverdue(task, now)   // Active, not completed, effectiveDate < now
predicates.IsDueSoon(task, now, days) // Active, not completed, now <= effectiveDate < threshold
predicates.IsUpcoming(task, now, days) // Everything else
```

### scopes/ - SQL Mirrors for Database Queries

GORM scope functions that produce the same results as predicates, but execute at the database level. Use these when counting or filtering large datasets without loading all records into memory.

```go
import "github.com/treytartt/honeydue-api/internal/task/scopes"

// State scopes
db.Scopes(scopes.ScopeActive)       // is_cancelled = false AND is_archived = false
db.Scopes(scopes.ScopeCompleted)    // next_due_date IS NULL AND EXISTS(completion)
db.Scopes(scopes.ScopeNotCompleted) // NOT (next_due_date IS NULL AND EXISTS(completion))
db.Scopes(scopes.ScopeInProgress)   // JOIN status WHERE name = 'In Progress'

// Date scopes (require time parameter)
db.Scopes(scopes.ScopeOverdue(now))           // COALESCE(next_due_date, due_date) < now
db.Scopes(scopes.ScopeDueSoon(now, 30))       // >= now AND < threshold
db.Scopes(scopes.ScopeUpcoming(now, 30))      // >= threshold OR no due date

// Filter scopes
db.Scopes(scopes.ScopeForResidence(id))       // residence_id = ?
db.Scopes(scopes.ScopeForResidences(ids))     // residence_id IN (?)

// Ordering
db.Scopes(scopes.ScopeKanbanOrder)            // Due date ASC, priority DESC, created DESC
```

### categorization/ - Chain of Responsibility Pattern

Determines which kanban column a task belongs to. Uses predicates internally.

```go
import "github.com/treytartt/honeydue-api/internal/task/categorization"

// Single task
column := categorization.CategorizeTask(task, 30)
columnStr := categorization.DetermineKanbanColumn(task, 30)

// Multiple tasks
columns := categorization.CategorizeTasksIntoColumns(tasks, 30)
// Returns map[KanbanColumn][]Task
```

### task.go - Convenient Re-exports

For most use cases, import the main task package which re-exports everything:

```go
import "github.com/treytartt/honeydue-api/internal/task"

// Use predicates
if task.IsCompleted(t) { ... }

// Use scopes
db.Scopes(task.ScopeOverdue(now)).Count(&count)

// Use categorization
column := task.CategorizeTask(t, 30)
```

## Canonical Rules (Single Source of Truth)

These rules are defined in `predicates/predicates.go` and enforced everywhere:

| Concept | Definition | SQL Equivalent |
|---------|------------|----------------|
| **Completed** | `NextDueDate == nil && len(Completions) > 0` | `next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion WHERE task_id = ?)` |
| **Active** | `!IsCancelled && !IsArchived` | `is_cancelled = false AND is_archived = false` |
| **In Progress** | `InProgress == true` | `in_progress = true` |
| **Effective Date** | `NextDueDate ?? DueDate` | `COALESCE(next_due_date, due_date)` |
| **Overdue** | `Active && !Completed && EffectiveDate < now` | Active + NotCompleted + `COALESCE(...) < ?` |
| **Due Soon** | `Active && !Completed && now <= EffectiveDate < threshold` | Active + NotCompleted + `COALESCE(...) >= ? AND COALESCE(...) < ?` |
| **Upcoming** | Everything else (future or no due date) | Active + NotCompleted + `COALESCE(...) >= ? OR (both NULL)` |

## Kanban Column Priority Order

When categorizing a task, the chain evaluates in this priority order:

1. **Cancelled** (highest) - `IsCancelled == true`
2. **Completed** - `NextDueDate == nil && len(Completions) > 0`
3. **In Progress** - `InProgress == true`
4. **Overdue** - `EffectiveDate < now`
5. **Due Soon** - `now <= EffectiveDate < threshold`
6. **Upcoming** (lowest/default) - Everything else

## Usage Examples

### Counting Overdue Tasks (Efficient)

```go
// Use scopes for database-level counting
var count int64
db.Model(&models.Task{}).
    Scopes(task.ScopeForResidences(residenceIDs), task.ScopeOverdue(now)).
    Count(&count)
```

### Building a Kanban Board

```go
// Load tasks with preloads
var tasks []models.Task
db.Preload("Completions").
    Scopes(task.ScopeForResidence(residenceID)).
    Find(&tasks)

// Categorize in memory using predicates
columns := task.CategorizeTasksIntoColumns(tasks, 30)
```

### Checking Task State in Business Logic

```go
// Use predicates for in-memory checks
if task.IsCompleted(t) {
    return []string{} // No actions for completed tasks
}

if task.IsOverdue(t, time.Now().UTC()) {
    return []string{"edit", "complete", "cancel", "mark_in_progress"}
}
```

### Button Types for a Task

```go
func GetButtonTypesForTask(t *models.Task, daysThreshold int) []string {
    now := time.Now().UTC()

    if predicates.IsCancelled(t) {
        return []string{"uncancel", "delete"}
    }
    if predicates.IsCompleted(t) {
        return []string{} // read-only
    }
    if predicates.IsInProgress(t) {
        return []string{"edit", "complete", "cancel"}
    }
    // Overdue, Due Soon, Upcoming all get the same buttons
    return []string{"edit", "complete", "cancel", "mark_in_progress"}
}
```

## Why Two Layers (Predicates + Scopes)?

| Layer | Use Case | Performance |
|-------|----------|-------------|
| **Predicates** | In-memory checks on loaded objects | Fast for small sets, already loaded data |
| **Scopes** | Database-level filtering/counting | Efficient for large datasets, avoids loading all records |

**Example**: Counting overdue tasks across 1000 residences:
- Predicates: Load all tasks into memory, filter in Go
- Scopes: Execute `SELECT COUNT(*) WHERE ...` in database

## Consistency Guarantees

1. **Scopes mirror predicates** - Each scope produces the same logical result as its predicate counterpart
2. **Tests verify consistency** - Unit tests ensure predicates and scopes produce identical results
3. **Cross-reference comments** - Each scope has a comment linking to its predicate equivalent
4. **Single import point** - The `task` package re-exports everything, making it easy to use consistently

## Files That Were Refactored

The following files were updated to use the consolidated task logic:

| File | Changes |
|------|---------|
| `internal/repositories/task_repo.go` | Removed duplicate `isTaskCompleted()`, uses categorization and scopes |
| `internal/services/task_button_types.go` | Uses predicates instead of inline logic |
| `internal/dto/responses/task.go` | `DetermineKanbanColumn` delegates to categorization package |
| `internal/worker/jobs/handler.go` | SQL queries documented with predicate references |
| `internal/admin/handlers/dashboard_handler.go` | Uses scopes for task statistics |
| `internal/services/residence_service.go` | Uses predicates for report generation |
| `internal/models/task.go` | `IsOverdue()` and `IsDueSoon()` fixed to use EffectiveDate |

## Migration Notes

### Old Pattern (Avoid)

```go
// DON'T: Inline logic that may drift
if task.NextDueDate == nil && len(task.Completions) > 0 {
    // completed...
}
```

### New Pattern (Preferred)

```go
// DO: Use predicates
if predicates.IsCompleted(task) {
    // completed...
}

// Or via the task package
if task.IsCompleted(t) {
    // completed...
}
```

## Developer Checklist: Adding Task-Related Features

**BEFORE writing any task-related code, ask yourself:**

### 1. Does this logic already exist?

Check `internal/task/predicates/predicates.go` for:
- [ ] State checks: `IsCompleted`, `IsActive`, `IsCancelled`, `IsArchived`, `IsInProgress`
- [ ] Date logic: `EffectiveDate`, `IsOverdue`, `IsDueSoon`, `IsUpcoming`
- [ ] Completion helpers: `HasCompletions`, `CompletionCount`, `IsRecurring`

### 2. Am I querying tasks from the database?

Use scopes from `internal/task/scopes/scopes.go`:
- [ ] Filtering by state: `ScopeActive`, `ScopeCompleted`, `ScopeNotCompleted`
- [ ] Filtering by date: `ScopeOverdue(now)`, `ScopeDueSoon(now, days)`, `ScopeUpcoming(now, days)`
- [ ] Filtering by residence: `ScopeForResidence(id)`, `ScopeForResidences(ids)`
- [ ] Ordering: `ScopeKanbanOrder`

### 3. Am I categorizing tasks into kanban columns?

Use `internal/task/categorization`:
- [ ] Single task: `categorization.CategorizeTask(task, daysThreshold)`
- [ ] Multiple tasks: `categorization.CategorizeTasksIntoColumns(tasks, daysThreshold)`

### 4. Am I writing inline task logic?

**STOP!** If you're writing any of these patterns inline, use the existing functions instead:

```go
// BAD: Inline completion check
if task.NextDueDate == nil && len(task.Completions) > 0 { ... }

// GOOD: Use predicate
if predicates.IsCompleted(task) { ... }

// BAD: Inline SQL for overdue
db.Where("due_date < ?", now)

// GOOD: Use scope
db.Scopes(scopes.ScopeOverdue(now))

// BAD: Manual kanban categorization
if task.IsCancelled { return "cancelled" }
else if ... { return "completed" }

// GOOD: Use categorization chain
column := categorization.CategorizeTask(task, 30)
```

### 5. Am I adding a NEW task state or concept?

If the existing predicates don't cover your use case:
1. **Add the predicate first** in `predicates/predicates.go`
2. **Add the corresponding scope** in `scopes/scopes.go` (if DB queries needed)
3. **Update the categorization chain** if it affects kanban columns
4. **Add tests** for both predicate and scope
5. **Update this documentation**

## Common Pitfalls

### PostgreSQL DATE vs TIMESTAMP Comparison

When comparing DATE columns with time parameters, GORM passes `time.Time` as a string. PostgreSQL then compares dates, not timestamps:

```sql
-- This compares DATES (wrong for time-of-day precision):
'2025-12-07'::date < '2025-12-07 17:00:00'  -- FALSE!

-- This compares TIMESTAMPS (correct):
'2025-12-07'::date < '2025-12-07 17:00:00'::timestamp  -- TRUE
```

**Solution**: All date scopes use explicit `::timestamp` casts:
```go
Where("COALESCE(next_due_date, due_date)::timestamp < ?::timestamp", now)
```

### Forgetting to Preload Completions

The `IsCompleted` predicate checks `len(task.Completions) > 0`. If you query tasks without preloading completions, this will always return false:

```go
// BAD: Completions not loaded
db.Find(&tasks)
predicates.IsCompleted(task) // Always false!

// GOOD: Preload completions
db.Preload("Completions").Find(&tasks)
predicates.IsCompleted(task) // Correct result
```

### In Progress Boolean Field

The `IsInProgress` predicate now uses a simple boolean field instead of a Status relation:

```go
// IsInProgress uses the in_progress boolean field directly
// No preloading required for this check
predicates.IsInProgress(task) // Checks task.InProgress boolean
```

## Quick Reference Import

For most files, use the convenience re-exports:

```go
import "github.com/treytartt/honeydue-api/internal/task"

// Then use:
task.IsCompleted(t)
task.ScopeOverdue(now)
task.CategorizeTask(t, 30)
```

For files that only need predicates or only need scopes:

```go
import "github.com/treytartt/honeydue-api/internal/task/predicates"
import "github.com/treytartt/honeydue-api/internal/task/scopes"
```

## Related Documentation

- `docs/TASK_KANBAN_CATEGORIZATION.md` - Detailed kanban column logic
- `docs/TASK_KANBAN_LOGIC.md` - Original kanban implementation notes
