Migrate from Gin to Echo framework and add comprehensive integration tests
Major changes: - Migrate all handlers from Gin to Echo framework - Add new apperrors, echohelpers, and validator packages - Update middleware for Echo compatibility - Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column) - Add 6 new integration tests: - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks - MultiUserSharing: Complex sharing with user removal - TaskStateTransitions: All state transitions and kanban column changes - DateBoundaryEdgeCases: Threshold boundary testing - CascadeOperations: Residence deletion cascade effects - MultiUserOperations: Shared residence collaboration - Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.) - Fix RemoveUser route param mismatch (userId -> user_id) - Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/treytartt/casera-api/internal/config"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/push"
|
||||
"github.com/treytartt/casera-api/internal/repositories"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
@@ -29,6 +30,7 @@ const (
|
||||
// Handler handles background job processing
|
||||
type Handler struct {
|
||||
db *gorm.DB
|
||||
taskRepo *repositories.TaskRepository
|
||||
pushClient *push.Client
|
||||
emailService *services.EmailService
|
||||
notificationService *services.NotificationService
|
||||
@@ -46,6 +48,7 @@ func NewHandler(db *gorm.DB, pushClient *push.Client, emailService *services.Ema
|
||||
|
||||
return &Handler{
|
||||
db: db,
|
||||
taskRepo: repositories.NewTaskRepository(db),
|
||||
pushClient: pushClient,
|
||||
emailService: emailService,
|
||||
notificationService: notificationService,
|
||||
@@ -72,8 +75,6 @@ func (h *Handler) HandleTaskReminder(ctx context.Context, task *asynq.Task) erro
|
||||
now := time.Now().UTC()
|
||||
currentHour := now.Hour()
|
||||
systemDefaultHour := h.config.Worker.TaskReminderHour
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
dayAfterTomorrow := today.AddDate(0, 0, 2)
|
||||
|
||||
log.Info().Int("current_hour", currentHour).Int("system_default_hour", systemDefaultHour).Msg("Task reminder check")
|
||||
|
||||
@@ -112,22 +113,18 @@ 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
|
||||
// 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("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 (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).
|
||||
Find(&dueSoonTasks).Error
|
||||
// Step 2: Query tasks due today or tomorrow using the single-purpose repository function
|
||||
// Uses the same scopes as kanban for consistency, with IncludeInProgress=true
|
||||
// so users still get notified about in-progress tasks that are due soon.
|
||||
opts := repositories.TaskFilterOptions{
|
||||
UserIDs: eligibleUserIDs,
|
||||
IncludeInProgress: true, // Notifications should include in-progress tasks
|
||||
PreloadResidence: true,
|
||||
PreloadCompletions: true,
|
||||
}
|
||||
|
||||
// Due soon = due within 2 days (today and tomorrow)
|
||||
dueSoonTasks, err := h.taskRepo.GetDueSoonTasks(now, 2, opts)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to query tasks due soon")
|
||||
return err
|
||||
@@ -176,7 +173,6 @@ func (h *Handler) HandleOverdueReminder(ctx context.Context, task *asynq.Task) e
|
||||
now := time.Now().UTC()
|
||||
currentHour := now.Hour()
|
||||
systemDefaultHour := h.config.Worker.OverdueReminderHour
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
log.Info().Int("current_hour", currentHour).Int("system_default_hour", systemDefaultHour).Msg("Overdue reminder check")
|
||||
|
||||
@@ -215,21 +211,17 @@ 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
|
||||
// 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("Completions").Preload("Residence").
|
||||
Where("due_date < ? OR next_due_date < ?", today, today).
|
||||
Where("is_cancelled = false").
|
||||
Where("is_archived = false").
|
||||
// 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).
|
||||
Find(&overdueTasks).Error
|
||||
// Step 2: Query overdue tasks using the single-purpose repository function
|
||||
// Uses the same scopes as kanban for consistency, with IncludeInProgress=true
|
||||
// so users still get notified about in-progress tasks that are overdue.
|
||||
opts := repositories.TaskFilterOptions{
|
||||
UserIDs: eligibleUserIDs,
|
||||
IncludeInProgress: true, // Notifications should include in-progress tasks
|
||||
PreloadResidence: true,
|
||||
PreloadCompletions: true,
|
||||
}
|
||||
|
||||
overdueTasks, err := h.taskRepo.GetOverdueTasks(now, opts)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to query overdue tasks")
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user