From 7dc85aa4cf58c0a67b6d744a1af8b89e807b97f0 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 2 Dec 2025 21:51:21 -0600 Subject: [PATCH] Add comprehensive kanban board tests and fix test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 13 tests for task kanban categorization and button types - Fix i18n initialization in test setup (was causing nil pointer panics) - Add TaskCompletionImage to test DB auto-migrate - Update ListTasks tests to expect kanban board response instead of array 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/handlers/task_handler_test.go | 27 +- internal/repositories/task_repo_test.go | 522 ++++++++++++++++++++++++ internal/services/task_service_test.go | 8 +- internal/testutil/testutil.go | 10 + 4 files changed, 560 insertions(+), 7 deletions(-) diff --git a/internal/handlers/task_handler_test.go b/internal/handlers/task_handler_test.go index 8d734ad..7a2bb47 100644 --- a/internal/handlers/task_handler_test.go +++ b/internal/handlers/task_handler_test.go @@ -168,11 +168,23 @@ func TestTaskHandler_ListTasks(t *testing.T) { testutil.AssertStatusCode(t, w, http.StatusOK) - var response []map[string]interface{} + // ListTasks returns a kanban board object, not an array + var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) - assert.Len(t, response, 3) + // Verify kanban structure + assert.Contains(t, response, "columns") + assert.Contains(t, response, "days_threshold") + + // Count total tasks across all columns + columns := response["columns"].([]interface{}) + totalTasks := 0 + for _, col := range columns { + column := col.(map[string]interface{}) + totalTasks += int(column["count"].(float64)) + } + assert.Equal(t, 3, totalTasks) }) } @@ -651,16 +663,19 @@ func TestTaskHandler_JSONResponses(t *testing.T) { assert.IsType(t, false, response["is_archived"]) }) - t.Run("list response returns array", func(t *testing.T) { + t.Run("list response returns kanban board", func(t *testing.T) { w := testutil.MakeRequest(router, "GET", "/api/tasks/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) - var response []map[string]interface{} + // ListTasks returns a kanban board object with columns + var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) - // Response should be an array of tasks - assert.IsType(t, []map[string]interface{}{}, response) + // Response should be a kanban board object + assert.Contains(t, response, "columns") + assert.Contains(t, response, "days_threshold") + assert.IsType(t, []interface{}{}, response["columns"]) }) } diff --git a/internal/repositories/task_repo_test.go b/internal/repositories/task_repo_test.go index a162a4b..df3c34b 100644 --- a/internal/repositories/task_repo_test.go +++ b/internal/repositories/task_repo_test.go @@ -313,3 +313,525 @@ func TestTaskRepository_CountByResidence(t *testing.T) { assert.Equal(t, int64(3), count) } +// === Kanban Board Categorization Tests === + +func TestKanbanBoard_CancelledTasksGoToCancelledColumn(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a cancelled task + task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Cancelled Task") + repo.Cancel(task.ID) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find cancelled column + var cancelledColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "cancelled_tasks" { + cancelledColumn = &board.Columns[i] + break + } + } + + require.NotNil(t, cancelledColumn, "cancelled_tasks column should exist") + assert.Equal(t, 1, cancelledColumn.Count) + assert.Len(t, cancelledColumn.Tasks, 1) + assert.Equal(t, "Cancelled Task", cancelledColumn.Tasks[0].Title) + + // Verify button types for cancelled column + assert.ElementsMatch(t, []string{"uncancel", "delete"}, cancelledColumn.ButtonTypes) +} + +func TestKanbanBoard_CompletedTasksGoToCompletedColumn(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a task with a completion + task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Completed Task") + completion := &models.TaskCompletion{ + TaskID: task.ID, + CompletedByID: user.ID, + CompletedAt: time.Now().UTC(), + Notes: "Done!", + } + err := repo.CreateCompletion(completion) + require.NoError(t, err) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find completed column + var completedColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "completed_tasks" { + completedColumn = &board.Columns[i] + break + } + } + + require.NotNil(t, completedColumn, "completed_tasks column should exist") + assert.Equal(t, 1, completedColumn.Count) + assert.Len(t, completedColumn.Tasks, 1) + assert.Equal(t, "Completed Task", completedColumn.Tasks[0].Title) + + // Verify button types for completed column (view only) + assert.ElementsMatch(t, []string{"view"}, completedColumn.ButtonTypes) +} + +func TestKanbanBoard_InProgressTasksGoToInProgressColumn(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Get "In Progress" status + var inProgressStatus models.TaskStatus + db.Where("name = ?", "In Progress").First(&inProgressStatus) + + // Create a task with "In Progress" status + task := &models.Task{ + ResidenceID: residence.ID, + CreatedByID: user.ID, + Title: "In Progress Task", + StatusID: &inProgressStatus.ID, + } + err := db.Create(task).Error + require.NoError(t, err) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find in_progress column + var inProgressColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "in_progress_tasks" { + inProgressColumn = &board.Columns[i] + break + } + } + + require.NotNil(t, inProgressColumn, "in_progress_tasks column should exist") + assert.Equal(t, 1, inProgressColumn.Count) + assert.Len(t, inProgressColumn.Tasks, 1) + assert.Equal(t, "In Progress Task", inProgressColumn.Tasks[0].Title) + + // Verify button types for in-progress column (no mark_in_progress since already in progress) + assert.ElementsMatch(t, []string{"edit", "complete", "cancel"}, inProgressColumn.ButtonTypes) +} + +func TestKanbanBoard_OverdueTasksGoToOverdueColumn(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a task with due date in the past + pastDue := time.Now().UTC().AddDate(0, 0, -5) // 5 days ago + task := &models.Task{ + ResidenceID: residence.ID, + CreatedByID: user.ID, + Title: "Overdue Task", + DueDate: &pastDue, + } + err := db.Create(task).Error + require.NoError(t, err) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find overdue column + var overdueColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "overdue_tasks" { + overdueColumn = &board.Columns[i] + break + } + } + + require.NotNil(t, overdueColumn, "overdue_tasks column should exist") + assert.Equal(t, 1, overdueColumn.Count) + assert.Len(t, overdueColumn.Tasks, 1) + assert.Equal(t, "Overdue Task", overdueColumn.Tasks[0].Title) + + // Verify button types for overdue column + assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, overdueColumn.ButtonTypes) +} + +func TestKanbanBoard_DueSoonTasksGoToDueSoonColumn(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a task with due date within threshold (default 30 days) + dueSoon := time.Now().UTC().AddDate(0, 0, 15) // 15 days from now + task := &models.Task{ + ResidenceID: residence.ID, + CreatedByID: user.ID, + Title: "Due Soon Task", + DueDate: &dueSoon, + } + err := db.Create(task).Error + require.NoError(t, err) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find due_soon column + var dueSoonColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "due_soon_tasks" { + dueSoonColumn = &board.Columns[i] + break + } + } + + require.NotNil(t, dueSoonColumn, "due_soon_tasks column should exist") + assert.Equal(t, 1, dueSoonColumn.Count) + assert.Len(t, dueSoonColumn.Tasks, 1) + assert.Equal(t, "Due Soon Task", dueSoonColumn.Tasks[0].Title) + + // Verify button types for due_soon column + assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, dueSoonColumn.ButtonTypes) +} + +func TestKanbanBoard_UpcomingTasksGoToUpcomingColumn(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a task with due date beyond threshold (default 30 days) + upcoming := time.Now().UTC().AddDate(0, 0, 45) // 45 days from now + task := &models.Task{ + ResidenceID: residence.ID, + CreatedByID: user.ID, + Title: "Upcoming Task", + DueDate: &upcoming, + } + err := db.Create(task).Error + require.NoError(t, err) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find upcoming column + var upcomingColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "upcoming_tasks" { + upcomingColumn = &board.Columns[i] + break + } + } + + require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist") + assert.Equal(t, 1, upcomingColumn.Count) + assert.Len(t, upcomingColumn.Tasks, 1) + assert.Equal(t, "Upcoming Task", upcomingColumn.Tasks[0].Title) + + // Verify button types for upcoming column + assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, upcomingColumn.ButtonTypes) +} + +func TestKanbanBoard_TasksWithNoDueDateGoToUpcomingColumn(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a task with no due date + task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "No Due Date Task") + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find upcoming column + var upcomingColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "upcoming_tasks" { + upcomingColumn = &board.Columns[i] + break + } + } + + require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist") + assert.Equal(t, 1, upcomingColumn.Count) + assert.Equal(t, task.ID, upcomingColumn.Tasks[0].ID) +} + +func TestKanbanBoard_ArchivedTasksAreExcluded(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a regular task and an archived task + testutil.CreateTestTask(t, db, residence.ID, user.ID, "Regular Task") + archivedTask := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Archived Task") + repo.Archive(archivedTask.ID) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Count total tasks across all columns + totalTasks := 0 + for _, col := range board.Columns { + totalTasks += col.Count + } + + // Only the non-archived task should be in the board + assert.Equal(t, 1, totalTasks) +} + +func TestKanbanBoard_CategoryPriority_CancelledTakesPrecedence(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a task that is BOTH overdue AND cancelled + // Cancelled should take precedence + pastDue := time.Now().UTC().AddDate(0, 0, -5) + task := &models.Task{ + ResidenceID: residence.ID, + CreatedByID: user.ID, + Title: "Overdue and Cancelled Task", + DueDate: &pastDue, + IsCancelled: true, + } + err := db.Create(task).Error + require.NoError(t, err) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find cancelled column + var cancelledColumn *models.KanbanColumn + var overdueColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "cancelled_tasks" { + cancelledColumn = &board.Columns[i] + } + if board.Columns[i].Name == "overdue_tasks" { + overdueColumn = &board.Columns[i] + } + } + + // Task should be in cancelled, not overdue + assert.Equal(t, 1, cancelledColumn.Count, "Task should be in cancelled column") + assert.Equal(t, 0, overdueColumn.Count, "Task should NOT be in overdue column") +} + +func TestKanbanBoard_CategoryPriority_CompletedTakesPrecedenceOverInProgress(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Get "In Progress" status + var inProgressStatus models.TaskStatus + db.Where("name = ?", "In Progress").First(&inProgressStatus) + + // Create a task that has "In Progress" status AND a completion + // Completed should take precedence + task := &models.Task{ + ResidenceID: residence.ID, + CreatedByID: user.ID, + Title: "In Progress with Completion", + StatusID: &inProgressStatus.ID, + } + err := db.Create(task).Error + require.NoError(t, err) + + // Add a completion + completion := &models.TaskCompletion{ + TaskID: task.ID, + CompletedByID: user.ID, + CompletedAt: time.Now().UTC(), + } + err = repo.CreateCompletion(completion) + require.NoError(t, err) + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Find columns + var completedColumn, inProgressColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "completed_tasks" { + completedColumn = &board.Columns[i] + } + if board.Columns[i].Name == "in_progress_tasks" { + inProgressColumn = &board.Columns[i] + } + } + + // Task should be in completed, not in_progress + assert.Equal(t, 1, completedColumn.Count, "Task should be in completed column") + assert.Equal(t, 0, inProgressColumn.Count, "Task should NOT be in in_progress column") +} + +func TestKanbanBoard_DaysThresholdAffectsCategorization(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + // Create a task due in 10 days + dueIn10Days := time.Now().UTC().AddDate(0, 0, 10) + task := &models.Task{ + ResidenceID: residence.ID, + CreatedByID: user.ID, + Title: "Task Due in 10 Days", + DueDate: &dueIn10Days, + } + err := db.Create(task).Error + require.NoError(t, err) + + // With 30-day threshold, should be in "due_soon" + board30, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + var dueSoon30, upcoming30 *models.KanbanColumn + for i := range board30.Columns { + if board30.Columns[i].Name == "due_soon_tasks" { + dueSoon30 = &board30.Columns[i] + } + if board30.Columns[i].Name == "upcoming_tasks" { + upcoming30 = &board30.Columns[i] + } + } + assert.Equal(t, 1, dueSoon30.Count, "With 30-day threshold, task should be in due_soon") + assert.Equal(t, 0, upcoming30.Count, "With 30-day threshold, task should NOT be in upcoming") + + // With 5-day threshold, should be in "upcoming" + board5, err := repo.GetKanbanData(residence.ID, 5) + require.NoError(t, err) + + var dueSoon5, upcoming5 *models.KanbanColumn + for i := range board5.Columns { + if board5.Columns[i].Name == "due_soon_tasks" { + dueSoon5 = &board5.Columns[i] + } + if board5.Columns[i].Name == "upcoming_tasks" { + upcoming5 = &board5.Columns[i] + } + } + assert.Equal(t, 0, dueSoon5.Count, "With 5-day threshold, task should NOT be in due_soon") + assert.Equal(t, 1, upcoming5.Count, "With 5-day threshold, task should be in upcoming") +} + +func TestKanbanBoard_ColumnMetadata(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") + + board, err := repo.GetKanbanData(residence.ID, 30) + require.NoError(t, err) + + // Verify all 6 columns exist with correct metadata + expectedColumns := []struct { + name string + displayName string + color string + buttonTypes []string + iosIcon string + androidIcon string + }{ + {"overdue_tasks", "Overdue", "#FF3B30", []string{"edit", "complete", "cancel", "mark_in_progress"}, "exclamationmark.triangle", "Warning"}, + {"due_soon_tasks", "Due Soon", "#FF9500", []string{"edit", "complete", "cancel", "mark_in_progress"}, "clock", "Schedule"}, + {"upcoming_tasks", "Upcoming", "#007AFF", []string{"edit", "complete", "cancel", "mark_in_progress"}, "calendar", "Event"}, + {"in_progress_tasks", "In Progress", "#5856D6", []string{"edit", "complete", "cancel"}, "hammer", "Build"}, + {"completed_tasks", "Completed", "#34C759", []string{"view"}, "checkmark.circle", "CheckCircle"}, + {"cancelled_tasks", "Cancelled", "#8E8E93", []string{"uncancel", "delete"}, "xmark.circle", "Cancel"}, + } + + assert.Len(t, board.Columns, 6, "Board should have 6 columns") + + for i, expected := range expectedColumns { + col := board.Columns[i] + assert.Equal(t, expected.name, col.Name, "Column %d name mismatch", i) + assert.Equal(t, expected.displayName, col.DisplayName, "Column %d display name mismatch", i) + assert.Equal(t, expected.color, col.Color, "Column %d color mismatch", i) + assert.ElementsMatch(t, expected.buttonTypes, col.ButtonTypes, "Column %d button types mismatch", i) + assert.Equal(t, expected.iosIcon, col.Icons["ios"], "Column %d iOS icon mismatch", i) + assert.Equal(t, expected.androidIcon, col.Icons["android"], "Column %d Android icon mismatch", i) + } + + // Verify days threshold is returned + assert.Equal(t, 30, board.DaysThreshold) +} + +func TestKanbanBoard_MultipleResidences(t *testing.T) { + db := testutil.SetupTestDB(t) + repo := NewTaskRepository(db) + testutil.SeedLookupData(t, db) + + user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") + residence1 := testutil.CreateTestResidence(t, db, user.ID, "House 1") + residence2 := testutil.CreateTestResidence(t, db, user.ID, "House 2") + + // Create tasks in both residences + testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Task in House 1") + testutil.CreateTestTask(t, db, residence2.ID, user.ID, "Task in House 2") + + // Create a cancelled task in house 1 + cancelledTask := testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Cancelled in House 1") + repo.Cancel(cancelledTask.ID) + + board, err := repo.GetKanbanDataForMultipleResidences([]uint{residence1.ID, residence2.ID}, 30) + require.NoError(t, err) + + // Count total tasks + totalTasks := 0 + for _, col := range board.Columns { + totalTasks += col.Count + } + assert.Equal(t, 3, totalTasks, "Should have 3 tasks total across both residences") + + // Find upcoming and cancelled columns + var upcomingColumn, cancelledColumn *models.KanbanColumn + for i := range board.Columns { + if board.Columns[i].Name == "upcoming_tasks" { + upcomingColumn = &board.Columns[i] + } + if board.Columns[i].Name == "cancelled_tasks" { + cancelledColumn = &board.Columns[i] + } + } + + assert.Equal(t, 2, upcomingColumn.Count, "Should have 2 upcoming tasks") + assert.Equal(t, 1, cancelledColumn.Count, "Should have 1 cancelled task") +} + diff --git a/internal/services/task_service_test.go b/internal/services/task_service_test.go index b8b251c..f772644 100644 --- a/internal/services/task_service_test.go +++ b/internal/services/task_service_test.go @@ -151,7 +151,13 @@ func TestTaskService_ListTasks(t *testing.T) { resp, err := service.ListTasks(user.ID) require.NoError(t, err) - assert.Len(t, resp, 3) + // ListTasks returns a KanbanBoardResponse with columns + // Count total tasks across all columns + totalTasks := 0 + for _, col := range resp.Columns { + totalTasks += col.Count + } + assert.Equal(t, 3, totalTasks) } func TestTaskService_UpdateTask(t *testing.T) { diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index bf8c58c..b73ea7f 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "sync" "testing" "github.com/gin-gonic/gin" @@ -13,11 +14,19 @@ import ( "gorm.io/gorm" "gorm.io/gorm/logger" + "github.com/treytartt/casera-api/internal/i18n" "github.com/treytartt/casera-api/internal/models" ) +var i18nOnce sync.Once + // SetupTestDB creates an in-memory SQLite database for testing func SetupTestDB(t *testing.T) *gorm.DB { + // Initialize i18n once for all tests + i18nOnce.Do(func() { + i18n.Init() + }) + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) @@ -40,6 +49,7 @@ func SetupTestDB(t *testing.T) *gorm.DB { &models.TaskStatus{}, &models.TaskFrequency{}, &models.TaskCompletion{}, + &models.TaskCompletionImage{}, &models.Contractor{}, &models.ContractorSpecialty{}, &models.Document{},