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:
@@ -62,9 +62,9 @@ func (r *TaskRepository) applyFilterOptions(query *gorm.DB, opts TaskFilterOptio
|
|||||||
} else if len(opts.ResidenceIDs) > 0 {
|
} else if len(opts.ResidenceIDs) > 0 {
|
||||||
query = query.Where("task_task.residence_id IN ?", opts.ResidenceIDs)
|
query = query.Where("task_task.residence_id IN ?", opts.ResidenceIDs)
|
||||||
} else if len(opts.UserIDs) > 0 {
|
} 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(
|
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,
|
opts.UserIDs, opts.UserIDs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1303,6 +1303,118 @@ func TestTaskFilterOptions_UserIDs(t *testing.T) {
|
|||||||
assert.Equal(t, "User1 Overdue", tasks[0].Title)
|
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) {
|
func TestTaskFilterOptions_IncludeArchived(t *testing.T) {
|
||||||
db := testutil.SetupTestDB(t)
|
db := testutil.SetupTestDB(t)
|
||||||
repo := NewTaskRepository(db)
|
repo := NewTaskRepository(db)
|
||||||
|
|||||||
@@ -168,10 +168,10 @@ func (s *OnboardingEmailService) UsersNeedingNoResidenceEmail() ([]models.User,
|
|||||||
// 3. Have no residences
|
// 3. Have no residences
|
||||||
// 4. Haven't received this email type yet
|
// 4. Haven't received this email type yet
|
||||||
err := s.db.Raw(`
|
err := s.db.Raw(`
|
||||||
SELECT u.* FROM users u
|
SELECT u.* FROM auth_user u
|
||||||
LEFT JOIN residences r ON r.owner_id = u.id AND r.is_active = true
|
LEFT JOIN residence_residence r ON r.owner_id = u.id AND r.is_active = true
|
||||||
LEFT JOIN onboarding_emails oe ON oe.user_id = u.id AND oe.email_type = ?
|
LEFT JOIN onboarding_emails oe ON oe.user_id = u.id AND oe.email_type = ?
|
||||||
WHERE u.verified = true
|
WHERE u.is_active = true
|
||||||
AND u.date_joined < ?
|
AND u.date_joined < ?
|
||||||
AND r.id IS NULL
|
AND r.id IS NULL
|
||||||
AND oe.id IS NULL
|
AND oe.id IS NULL
|
||||||
@@ -197,15 +197,15 @@ func (s *OnboardingEmailService) UsersNeedingNoTasksEmail() ([]models.User, erro
|
|||||||
// 4. Have no tasks across ANY of their residences
|
// 4. Have no tasks across ANY of their residences
|
||||||
// 5. Haven't received this email type yet
|
// 5. Haven't received this email type yet
|
||||||
err := s.db.Raw(`
|
err := s.db.Raw(`
|
||||||
SELECT DISTINCT u.* FROM users u
|
SELECT DISTINCT u.* FROM auth_user u
|
||||||
INNER JOIN residences r ON r.owner_id = u.id AND r.is_active = true
|
INNER JOIN residence_residence r ON r.owner_id = u.id AND r.is_active = true
|
||||||
LEFT JOIN tasks t ON t.residence_id IN (SELECT id FROM residences WHERE owner_id = u.id AND is_active = true) AND t.is_active = true
|
LEFT JOIN task_task t ON t.residence_id IN (SELECT id FROM residence_residence WHERE owner_id = u.id AND is_active = true) AND t.is_cancelled = false AND t.is_archived = false
|
||||||
LEFT JOIN onboarding_emails oe ON oe.user_id = u.id AND oe.email_type = ?
|
LEFT JOIN onboarding_emails oe ON oe.user_id = u.id AND oe.email_type = ?
|
||||||
WHERE u.verified = true
|
WHERE u.is_active = true
|
||||||
AND t.id IS NULL
|
AND t.id IS NULL
|
||||||
AND oe.id IS NULL
|
AND oe.id IS NULL
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1 FROM residences r2
|
SELECT 1 FROM residence_residence r2
|
||||||
WHERE r2.owner_id = u.id
|
WHERE r2.owner_id = u.id
|
||||||
AND r2.is_active = true
|
AND r2.is_active = true
|
||||||
AND r2.created_at < ?
|
AND r2.created_at < ?
|
||||||
|
|||||||
@@ -338,9 +338,9 @@ func (h *Handler) HandleDailyDigest(ctx context.Context, task *asynq.Task) error
|
|||||||
THEN t.id
|
THEN t.id
|
||||||
END) as due_this_week
|
END) as due_this_week
|
||||||
FROM auth_user u
|
FROM auth_user u
|
||||||
JOIN residence_residence r ON r.owner_id = u.id OR r.id IN (
|
JOIN residence_residence r ON (r.owner_id = u.id OR r.id IN (
|
||||||
SELECT residence_id FROM residence_residence_users WHERE user_id = u.id
|
SELECT residence_id FROM residence_residence_users WHERE user_id = u.id
|
||||||
)
|
)) AND r.is_active = true
|
||||||
JOIN task_task t ON t.residence_id = r.id
|
JOIN task_task t ON t.residence_id = r.id
|
||||||
AND t.is_cancelled = false
|
AND t.is_cancelled = false
|
||||||
AND t.is_archived = false
|
AND t.is_archived = false
|
||||||
|
|||||||
Reference in New Issue
Block a user