Files
honeyDueAPI/internal/task/task.go
Trey t 0cf64cfb0c Add performance optimizations and database indexes
Database Indexes (migrations 006-009):
- Add case-insensitive indexes for auth lookups (email, username)
- Add composite indexes for task kanban queries
- Add indexes for notification, document, and completion queries
- Add unique index for active share codes
- Remove redundant idx_share_code_active and idx_notification_user_sent

Repository Optimizations:
- Add FindResidenceIDsByUser() lightweight method (IDs only, no preloads)
- Optimize GetResidenceUsers() with single UNION query (was 2 queries)
- Optimize kanban completion preloads to minimal columns (id, task_id, completed_at)

Service Optimizations:
- Remove Category/Priority/Frequency preloads from task queries
- Remove summary calculations from CRUD responses (client calculates)
- Use lightweight FindResidenceIDsByUser() instead of full FindByUser()

These changes reduce database load and response times for common operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 01:06:08 -06:00

261 lines
8.8 KiB
Go

// Package task provides consolidated task domain logic.
//
// This package serves as the single entry point for all task-related business logic.
// It re-exports functions from sub-packages for convenient imports.
//
// Architecture:
//
// predicates/ - Pure Go predicate functions (SINGLE SOURCE OF TRUTH)
// scopes/ - GORM scope functions (SQL mirrors of predicates)
// categorization/ - Chain of Responsibility for kanban categorization
//
// Usage:
//
// import "github.com/treytartt/casera-api/internal/task"
//
// // Use predicates for in-memory checks
// if task.IsCompleted(myTask) { ... }
//
// // Use scopes for database queries
// db.Scopes(task.ScopeOverdue(now)).Find(&tasks)
//
// // Use categorization for kanban column determination
// column := task.CategorizeTask(myTask, 30)
//
// For more details, see docs/TASK_LOGIC_ARCHITECTURE.md
package task
import (
"time"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/task/categorization"
"github.com/treytartt/casera-api/internal/task/predicates"
"github.com/treytartt/casera-api/internal/task/scopes"
"gorm.io/gorm"
)
// =============================================================================
// RE-EXPORTED TYPES
// =============================================================================
// KanbanColumn represents the possible kanban column names
type KanbanColumn = categorization.KanbanColumn
// Column constants
const (
ColumnOverdue = categorization.ColumnOverdue
ColumnDueSoon = categorization.ColumnDueSoon
ColumnUpcoming = categorization.ColumnUpcoming
ColumnInProgress = categorization.ColumnInProgress
ColumnCompleted = categorization.ColumnCompleted
ColumnCancelled = categorization.ColumnCancelled
)
// =============================================================================
// RE-EXPORTED PREDICATES
// These are the SINGLE SOURCE OF TRUTH for task logic
// =============================================================================
// IsCompleted returns true if a task is considered "completed" per kanban rules.
// A task is completed when NextDueDate is nil AND has at least one completion.
func IsCompleted(task *models.Task) bool {
return predicates.IsCompleted(task)
}
// IsActive returns true if the task is not cancelled and not archived.
func IsActive(task *models.Task) bool {
return predicates.IsActive(task)
}
// IsCancelled returns true if the task has been cancelled.
func IsCancelled(task *models.Task) bool {
return predicates.IsCancelled(task)
}
// IsArchived returns true if the task has been archived.
func IsArchived(task *models.Task) bool {
return predicates.IsArchived(task)
}
// IsInProgress returns true if the task has status "In Progress".
func IsInProgress(task *models.Task) bool {
return predicates.IsInProgress(task)
}
// EffectiveDate returns the date used for scheduling calculations.
// Prefers NextDueDate, falls back to DueDate.
func EffectiveDate(task *models.Task) *time.Time {
return predicates.EffectiveDate(task)
}
// IsOverdue returns true if the task's effective date is in the past.
func IsOverdue(task *models.Task, now time.Time) bool {
return predicates.IsOverdue(task, now)
}
// IsDueSoon returns true if the task's effective date is within the threshold.
func IsDueSoon(task *models.Task, now time.Time, daysThreshold int) bool {
return predicates.IsDueSoon(task, now, daysThreshold)
}
// IsUpcoming returns true if the task is due after the threshold or has no due date.
func IsUpcoming(task *models.Task, now time.Time, daysThreshold int) bool {
return predicates.IsUpcoming(task, now, daysThreshold)
}
// HasCompletions returns true if the task has at least one completion record.
func HasCompletions(task *models.Task) bool {
return predicates.HasCompletions(task)
}
// GetCompletionCount returns the number of completions for a task.
// Supports both preloaded Completions slice and computed CompletionCount field.
func GetCompletionCount(task *models.Task) int {
return predicates.GetCompletionCount(task)
}
// IsRecurring returns true if the task has a recurring frequency.
func IsRecurring(task *models.Task) bool {
return predicates.IsRecurring(task)
}
// IsOneTime returns true if the task is a one-time (non-recurring) task.
func IsOneTime(task *models.Task) bool {
return predicates.IsOneTime(task)
}
// =============================================================================
// RE-EXPORTED SCOPES
// These are SQL mirrors of the predicates for database queries
// =============================================================================
// ScopeActive filters to tasks that are not cancelled and not archived.
func ScopeActive(db *gorm.DB) *gorm.DB {
return scopes.ScopeActive(db)
}
// ScopeCancelled filters to cancelled tasks only.
func ScopeCancelled(db *gorm.DB) *gorm.DB {
return scopes.ScopeCancelled(db)
}
// ScopeArchived filters to archived tasks only.
func ScopeArchived(db *gorm.DB) *gorm.DB {
return scopes.ScopeArchived(db)
}
// ScopeCompleted filters to completed tasks.
func ScopeCompleted(db *gorm.DB) *gorm.DB {
return scopes.ScopeCompleted(db)
}
// ScopeNotCompleted excludes completed tasks.
func ScopeNotCompleted(db *gorm.DB) *gorm.DB {
return scopes.ScopeNotCompleted(db)
}
// ScopeInProgress filters to tasks with status "In Progress".
func ScopeInProgress(db *gorm.DB) *gorm.DB {
return scopes.ScopeInProgress(db)
}
// ScopeNotInProgress excludes tasks with status "In Progress".
func ScopeNotInProgress(db *gorm.DB) *gorm.DB {
return scopes.ScopeNotInProgress(db)
}
// ScopeOverdue returns a scope for overdue tasks.
func ScopeOverdue(now time.Time) func(db *gorm.DB) *gorm.DB {
return scopes.ScopeOverdue(now)
}
// ScopeDueSoon returns a scope for tasks due within the threshold.
func ScopeDueSoon(now time.Time, daysThreshold int) func(db *gorm.DB) *gorm.DB {
return scopes.ScopeDueSoon(now, daysThreshold)
}
// ScopeUpcoming returns a scope for tasks due after the threshold or with no due date.
func ScopeUpcoming(now time.Time, daysThreshold int) func(db *gorm.DB) *gorm.DB {
return scopes.ScopeUpcoming(now, daysThreshold)
}
// ScopeDueInRange returns a scope for tasks with effective date in a range.
func ScopeDueInRange(start, end time.Time) func(db *gorm.DB) *gorm.DB {
return scopes.ScopeDueInRange(start, end)
}
// ScopeHasDueDate filters to tasks that have an effective due date.
func ScopeHasDueDate(db *gorm.DB) *gorm.DB {
return scopes.ScopeHasDueDate(db)
}
// ScopeNoDueDate filters to tasks that have no effective due date.
func ScopeNoDueDate(db *gorm.DB) *gorm.DB {
return scopes.ScopeNoDueDate(db)
}
// ScopeForResidence filters tasks by a single residence ID.
func ScopeForResidence(residenceID uint) func(db *gorm.DB) *gorm.DB {
return scopes.ScopeForResidence(residenceID)
}
// ScopeForResidences filters tasks by multiple residence IDs.
func ScopeForResidences(residenceIDs []uint) func(db *gorm.DB) *gorm.DB {
return scopes.ScopeForResidences(residenceIDs)
}
// ScopeHasCompletions filters to tasks that have at least one completion.
func ScopeHasCompletions(db *gorm.DB) *gorm.DB {
return scopes.ScopeHasCompletions(db)
}
// ScopeNoCompletions filters to tasks that have no completions.
func ScopeNoCompletions(db *gorm.DB) *gorm.DB {
return scopes.ScopeNoCompletions(db)
}
// ScopeOrderByDueDate orders tasks by effective due date ascending, nulls last.
func ScopeOrderByDueDate(db *gorm.DB) *gorm.DB {
return scopes.ScopeOrderByDueDate(db)
}
// ScopeOrderByPriority orders tasks by priority level descending (urgent first).
func ScopeOrderByPriority(db *gorm.DB) *gorm.DB {
return scopes.ScopeOrderByPriority(db)
}
// ScopeOrderByCreatedAt orders tasks by creation date descending (newest first).
func ScopeOrderByCreatedAt(db *gorm.DB) *gorm.DB {
return scopes.ScopeOrderByCreatedAt(db)
}
// ScopeKanbanOrder applies the standard kanban ordering.
func ScopeKanbanOrder(db *gorm.DB) *gorm.DB {
return scopes.ScopeKanbanOrder(db)
}
// =============================================================================
// RE-EXPORTED CATEGORIZATION
// =============================================================================
// CategorizeTask determines which kanban column a task belongs to.
func CategorizeTask(task *models.Task, daysThreshold int) KanbanColumn {
return categorization.CategorizeTask(task, daysThreshold)
}
// DetermineKanbanColumn is a convenience function that returns the column as a string.
func DetermineKanbanColumn(task *models.Task, daysThreshold int) string {
return categorization.DetermineKanbanColumn(task, daysThreshold)
}
// CategorizeTasksIntoColumns categorizes multiple tasks into their respective columns.
func CategorizeTasksIntoColumns(tasks []models.Task, daysThreshold int) map[KanbanColumn][]models.Task {
return categorization.CategorizeTasksIntoColumns(tasks, daysThreshold)
}
// NewChain creates a new categorization chain for custom usage.
func NewChain() *categorization.Chain {
return categorization.NewChain()
}