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>
261 lines
8.8 KiB
Go
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()
|
|
}
|