Fix notification queries to exclude tasks from inactive residences

- task_repo.go: Add is_active filter to residence subquery in UserIDs filter
- handler.go: Add is_active filter to daily digest residence join
- onboarding_email_service.go: Fix Django table names and task status filter
- task_repo_test.go: Add regression tests for inactive residence filtering

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-22 14:47:37 -06:00
parent 8cd41a145f
commit 37a04db82e
4 changed files with 124 additions and 12 deletions

View File

@@ -62,9 +62,9 @@ func (r *TaskRepository) applyFilterOptions(query *gorm.DB, opts TaskFilterOptio
} else if len(opts.ResidenceIDs) > 0 {
query = query.Where("task_task.residence_id IN ?", opts.ResidenceIDs)
} else if len(opts.UserIDs) > 0 {
// For notifications: tasks assigned to users OR owned by users
// For notifications: tasks assigned to users OR owned by users (only from active residences)
query = query.Where(
"(task_task.assigned_to_id IN ? OR task_task.residence_id IN (SELECT id FROM residence_residence WHERE owner_id IN ?))",
"(task_task.assigned_to_id IN ? OR task_task.residence_id IN (SELECT id FROM residence_residence WHERE owner_id IN ? AND is_active = true))",
opts.UserIDs, opts.UserIDs,
)
}

View File

@@ -1303,6 +1303,118 @@ func TestTaskFilterOptions_UserIDs(t *testing.T) {
assert.Equal(t, "User1 Overdue", tasks[0].Title)
}
// TestTaskFilterOptions_UserIDs_ExcludesInactiveResidences verifies that tasks
// from inactive residences are excluded when filtering by user IDs.
// This is a regression test for the bug where users received notifications
// for tasks on residences they had deactivated.
func TestTaskFilterOptions_UserIDs_ExcludesInactiveResidences(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
// Create an active residence
activeResidence := testutil.CreateTestResidence(t, db, user.ID, "Active House")
// Create an inactive residence (manually, since testutil always creates active)
// Note: Must update after create because GORM's default:true overrides struct value
inactiveResidence := &models.Residence{
OwnerID: user.ID,
Name: "Inactive House",
StreetAddress: "456 Inactive St",
City: "Test City",
StateProvince: "TS",
PostalCode: "12345",
Country: "USA",
IsPrimary: false,
}
err := db.Create(inactiveResidence).Error
require.NoError(t, err)
// Set to inactive after creation to bypass GORM default
db.Model(inactiveResidence).Update("is_active", false)
now := time.Now().UTC()
pastDue := now.AddDate(0, 0, -5)
// Create overdue tasks in both residences
db.Create(&models.Task{
ResidenceID: activeResidence.ID,
CreatedByID: user.ID,
Title: "Overdue in Active Residence",
DueDate: &pastDue,
})
db.Create(&models.Task{
ResidenceID: inactiveResidence.ID,
CreatedByID: user.ID,
Title: "Overdue in Inactive Residence",
DueDate: &pastDue,
})
// Filter by user ID (used by notification system)
opts := TaskFilterOptions{UserIDs: []uint{user.ID}, IncludeInProgress: true}
tasks, err := repo.GetOverdueTasks(now, opts)
require.NoError(t, err)
// Should only return task from active residence
assert.Len(t, tasks, 1, "Should only return tasks from active residences")
assert.Equal(t, "Overdue in Active Residence", tasks[0].Title)
}
// TestTaskFilterOptions_UserIDs_DueSoon_ExcludesInactiveResidences verifies that
// due soon tasks from inactive residences are excluded when filtering by user IDs.
func TestTaskFilterOptions_UserIDs_DueSoon_ExcludesInactiveResidences(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
// Create an active residence
activeResidence := testutil.CreateTestResidence(t, db, user.ID, "Active House")
// Create an inactive residence
// Note: Must update after create because GORM's default:true overrides struct value
inactiveResidence := &models.Residence{
OwnerID: user.ID,
Name: "Inactive House",
StreetAddress: "456 Inactive St",
City: "Test City",
StateProvince: "TS",
PostalCode: "12345",
Country: "USA",
IsPrimary: false,
}
err := db.Create(inactiveResidence).Error
require.NoError(t, err)
// Set to inactive after creation to bypass GORM default
db.Model(inactiveResidence).Update("is_active", false)
now := time.Now().UTC()
dueSoon := now.AddDate(0, 0, 5) // 5 days from now
// Create due soon tasks in both residences
db.Create(&models.Task{
ResidenceID: activeResidence.ID,
CreatedByID: user.ID,
Title: "Due Soon in Active Residence",
DueDate: &dueSoon,
})
db.Create(&models.Task{
ResidenceID: inactiveResidence.ID,
CreatedByID: user.ID,
Title: "Due Soon in Inactive Residence",
DueDate: &dueSoon,
})
// Filter by user ID (used by notification system)
opts := TaskFilterOptions{UserIDs: []uint{user.ID}, IncludeInProgress: true}
tasks, err := repo.GetDueSoonTasks(now, 30, opts)
require.NoError(t, err)
// Should only return task from active residence
assert.Len(t, tasks, 1, "Should only return tasks from active residences")
assert.Equal(t, "Due Soon in Active Residence", tasks[0].Title)
}
func TestTaskFilterOptions_IncludeArchived(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)