diff --git a/internal/handlers/residence_handler.go b/internal/handlers/residence_handler.go index 2ac75fe..e7ca0bd 100644 --- a/internal/handlers/residence_handler.go +++ b/internal/handlers/residence_handler.go @@ -39,7 +39,7 @@ func (h *ResidenceHandler) ListResidences(c echo.Context) error { return err } - response, err := h.residenceService.ListResidences(user.ID) + response, err := h.residenceService.ListResidences(c.Request().Context(), user.ID) if err != nil { return err } @@ -55,7 +55,7 @@ func (h *ResidenceHandler) GetMyResidences(c echo.Context) error { } userNow := middleware.GetUserNow(c) - response, err := h.residenceService.GetMyResidences(user.ID, userNow) + response, err := h.residenceService.GetMyResidences(c.Request().Context(), user.ID, userNow) if err != nil { return err } @@ -72,7 +72,7 @@ func (h *ResidenceHandler) GetSummary(c echo.Context) error { } userNow := middleware.GetUserNow(c) - summary, err := h.residenceService.GetSummary(user.ID, userNow) + summary, err := h.residenceService.GetSummary(c.Request().Context(), user.ID, userNow) if err != nil { return err } @@ -93,7 +93,7 @@ func (h *ResidenceHandler) GetResidence(c echo.Context) error { } userNow := middleware.GetUserNow(c) - response, err := h.residenceService.GetResidence(uint(residenceID), user.ID, userNow) + response, err := h.residenceService.GetResidence(c.Request().Context(), uint(residenceID), user.ID, userNow) if err != nil { return err } @@ -116,7 +116,7 @@ func (h *ResidenceHandler) CreateResidence(c echo.Context) error { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } - response, err := h.residenceService.CreateResidence(&req, user.ID) + response, err := h.residenceService.CreateResidence(c.Request().Context(), &req, user.ID) if err != nil { return err } @@ -144,7 +144,7 @@ func (h *ResidenceHandler) UpdateResidence(c echo.Context) error { return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) } - response, err := h.residenceService.UpdateResidence(uint(residenceID), user.ID, &req) + response, err := h.residenceService.UpdateResidence(c.Request().Context(), uint(residenceID), user.ID, &req) if err != nil { return err } @@ -164,7 +164,7 @@ func (h *ResidenceHandler) DeleteResidence(c echo.Context) error { return apperrors.BadRequest("error.invalid_residence_id") } - response, err := h.residenceService.DeleteResidence(uint(residenceID), user.ID) + response, err := h.residenceService.DeleteResidence(c.Request().Context(), uint(residenceID), user.ID) if err != nil { return err } @@ -185,7 +185,7 @@ func (h *ResidenceHandler) GetShareCode(c echo.Context) error { return apperrors.BadRequest("error.invalid_residence_id") } - shareCode, err := h.residenceService.GetShareCode(uint(residenceID), user.ID) + shareCode, err := h.residenceService.GetShareCode(c.Request().Context(), uint(residenceID), user.ID) if err != nil { return err } @@ -213,7 +213,7 @@ func (h *ResidenceHandler) GenerateShareCode(c echo.Context) error { // Request body is optional c.Bind(&req) - response, err := h.residenceService.GenerateShareCode(uint(residenceID), user.ID, req.ExpiresInHours) + response, err := h.residenceService.GenerateShareCode(c.Request().Context(), uint(residenceID), user.ID, req.ExpiresInHours) if err != nil { return err } @@ -238,7 +238,7 @@ func (h *ResidenceHandler) GenerateSharePackage(c echo.Context) error { // Request body is optional (for expires_in_hours) c.Bind(&req) - response, err := h.residenceService.GenerateSharePackage(uint(residenceID), user.ID, req.ExpiresInHours) + response, err := h.residenceService.GenerateSharePackage(c.Request().Context(), uint(residenceID), user.ID, req.ExpiresInHours) if err != nil { return err } @@ -261,7 +261,7 @@ func (h *ResidenceHandler) JoinWithCode(c echo.Context) error { return err } - response, err := h.residenceService.JoinWithCode(req.Code, user.ID) + response, err := h.residenceService.JoinWithCode(c.Request().Context(), req.Code, user.ID) if err != nil { return err } @@ -281,7 +281,7 @@ func (h *ResidenceHandler) GetResidenceUsers(c echo.Context) error { return apperrors.BadRequest("error.invalid_residence_id") } - users, err := h.residenceService.GetResidenceUsers(uint(residenceID), user.ID) + users, err := h.residenceService.GetResidenceUsers(c.Request().Context(), uint(residenceID), user.ID) if err != nil { return err } @@ -306,7 +306,7 @@ func (h *ResidenceHandler) RemoveResidenceUser(c echo.Context) error { return apperrors.BadRequest("error.invalid_user_id") } - err = h.residenceService.RemoveUser(uint(residenceID), uint(userIDToRemove), user.ID) + err = h.residenceService.RemoveUser(c.Request().Context(), uint(residenceID), uint(userIDToRemove), user.ID) if err != nil { return err } @@ -316,7 +316,7 @@ func (h *ResidenceHandler) RemoveResidenceUser(c echo.Context) error { // GetResidenceTypes handles GET /api/residences/types/ func (h *ResidenceHandler) GetResidenceTypes(c echo.Context) error { - types, err := h.residenceService.GetResidenceTypes() + types, err := h.residenceService.GetResidenceTypes(c.Request().Context()) if err != nil { return err } @@ -348,7 +348,7 @@ func (h *ResidenceHandler) GenerateTasksReport(c echo.Context) error { c.Bind(&req) // Generate the report data - report, err := h.residenceService.GenerateTasksReport(uint(residenceID), user.ID) + report, err := h.residenceService.GenerateTasksReport(c.Request().Context(), uint(residenceID), user.ID) if err != nil { return err } diff --git a/internal/handlers/residence_handler_test.go b/internal/handlers/residence_handler_test.go index 772c9db..151b302 100644 --- a/internal/handlers/residence_handler_test.go +++ b/internal/handlers/residence_handler_test.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "encoding/json" "fmt" "net/http" @@ -324,7 +325,7 @@ func TestResidenceHandler_JoinWithCode(t *testing.T) { userRepo := repositories.NewUserRepository(db) cfg := &config.Config{} residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg) - shareResp, _ := residenceService.GenerateShareCode(residence.ID, owner.ID, 24) + shareResp, _ := residenceService.GenerateShareCode(context.Background(), residence.ID, owner.ID, 24) authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(newUser)) @@ -357,7 +358,7 @@ func TestResidenceHandler_JoinWithCode(t *testing.T) { t.Run("owner tries to join own residence", func(t *testing.T) { // Generate new code - shareResp2, _ := residenceService.GenerateShareCode(residence.ID, owner.ID, 24) + shareResp2, _ := residenceService.GenerateShareCode(context.Background(), residence.ID, owner.ID, 24) req := requests.JoinWithCodeRequest{ Code: shareResp2.ShareCode.Code, diff --git a/internal/handlers/static_data_handler.go b/internal/handlers/static_data_handler.go index 75b055b..46ddbe3 100644 --- a/internal/handlers/static_data_handler.go +++ b/internal/handlers/static_data_handler.go @@ -86,22 +86,22 @@ func (h *StaticDataHandler) GetStaticData(c echo.Context) error { } // Cache miss - fetch all data from services - residenceTypes, err := h.residenceService.GetResidenceTypes() + residenceTypes, err := h.residenceService.GetResidenceTypes(c.Request().Context()) if err != nil { return err } - taskCategories, err := h.taskService.GetCategories() + taskCategories, err := h.taskService.GetCategories(c.Request().Context()) if err != nil { return err } - taskPriorities, err := h.taskService.GetPriorities() + taskPriorities, err := h.taskService.GetPriorities(c.Request().Context()) if err != nil { return err } - taskFrequencies, err := h.taskService.GetFrequencies() + taskFrequencies, err := h.taskService.GetFrequencies(c.Request().Context()) if err != nil { return err } diff --git a/internal/handlers/task_handler.go b/internal/handlers/task_handler.go index 7645998..b061404 100644 --- a/internal/handlers/task_handler.go +++ b/internal/handlers/task_handler.go @@ -83,7 +83,7 @@ func (h *TaskHandler) GetTask(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - response, err := h.taskService.GetTask(uint(taskID), user.ID) + response, err := h.taskService.GetTask(c.Request().Context(), uint(taskID), user.ID) if err != nil { return err } @@ -144,7 +144,7 @@ func (h *TaskHandler) CreateTask(c echo.Context) error { return err } - response, err := h.taskService.CreateTask(&req, user.ID, userNow) + response, err := h.taskService.CreateTask(c.Request().Context(), &req, user.ID, userNow) if err != nil { return err } @@ -169,7 +169,7 @@ func (h *TaskHandler) BulkCreateTasks(c echo.Context) error { return err } - response, err := h.taskService.BulkCreateTasks(&req, user.ID, userNow) + response, err := h.taskService.BulkCreateTasks(c.Request().Context(), &req, user.ID, userNow) if err != nil { return err } @@ -197,7 +197,7 @@ func (h *TaskHandler) UpdateTask(c echo.Context) error { return err } - response, err := h.taskService.UpdateTask(uint(taskID), user.ID, &req, userNow) + response, err := h.taskService.UpdateTask(c.Request().Context(), uint(taskID), user.ID, &req, userNow) if err != nil { return err } @@ -215,7 +215,7 @@ func (h *TaskHandler) DeleteTask(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - response, err := h.taskService.DeleteTask(uint(taskID), user.ID) + response, err := h.taskService.DeleteTask(c.Request().Context(), uint(taskID), user.ID) if err != nil { return err } @@ -235,7 +235,7 @@ func (h *TaskHandler) MarkInProgress(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - response, err := h.taskService.MarkInProgress(uint(taskID), user.ID, userNow) + response, err := h.taskService.MarkInProgress(c.Request().Context(), uint(taskID), user.ID, userNow) if err != nil { return err } @@ -255,7 +255,7 @@ func (h *TaskHandler) CancelTask(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - response, err := h.taskService.CancelTask(uint(taskID), user.ID, userNow) + response, err := h.taskService.CancelTask(c.Request().Context(), uint(taskID), user.ID, userNow) if err != nil { return err } @@ -275,7 +275,7 @@ func (h *TaskHandler) UncancelTask(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - response, err := h.taskService.UncancelTask(uint(taskID), user.ID, userNow) + response, err := h.taskService.UncancelTask(c.Request().Context(), uint(taskID), user.ID, userNow) if err != nil { return err } @@ -295,7 +295,7 @@ func (h *TaskHandler) ArchiveTask(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - response, err := h.taskService.ArchiveTask(uint(taskID), user.ID, userNow) + response, err := h.taskService.ArchiveTask(c.Request().Context(), uint(taskID), user.ID, userNow) if err != nil { return err } @@ -315,7 +315,7 @@ func (h *TaskHandler) UnarchiveTask(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - response, err := h.taskService.UnarchiveTask(uint(taskID), user.ID, userNow) + response, err := h.taskService.UnarchiveTask(c.Request().Context(), uint(taskID), user.ID, userNow) if err != nil { return err } @@ -334,7 +334,7 @@ func (h *TaskHandler) QuickComplete(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - err = h.taskService.QuickComplete(uint(taskID), user.ID) + err = h.taskService.QuickComplete(c.Request().Context(), uint(taskID), user.ID) if err != nil { return err } @@ -354,7 +354,7 @@ func (h *TaskHandler) GetTaskCompletions(c echo.Context) error { return apperrors.BadRequest("error.invalid_task_id") } - response, err := h.taskService.GetCompletionsByTask(uint(taskID), user.ID) + response, err := h.taskService.GetCompletionsByTask(c.Request().Context(), uint(taskID), user.ID) if err != nil { return err } @@ -367,7 +367,7 @@ func (h *TaskHandler) ListCompletions(c echo.Context) error { if err != nil { return err } - response, err := h.taskService.ListCompletions(user.ID) + response, err := h.taskService.ListCompletions(c.Request().Context(), user.ID) if err != nil { return err } @@ -385,7 +385,7 @@ func (h *TaskHandler) GetCompletion(c echo.Context) error { return apperrors.BadRequest("error.invalid_completion_id") } - response, err := h.taskService.GetCompletion(uint(completionID), user.ID) + response, err := h.taskService.GetCompletion(c.Request().Context(), uint(completionID), user.ID) if err != nil { return err } @@ -465,7 +465,7 @@ func (h *TaskHandler) CreateCompletion(c echo.Context) error { return err } - response, err := h.taskService.CreateCompletion(&req, user.ID, userNow) + response, err := h.taskService.CreateCompletion(c.Request().Context(), &req, user.ID, userNow) if err != nil { return err } @@ -491,7 +491,7 @@ func (h *TaskHandler) UpdateCompletion(c echo.Context) error { return err } - response, err := h.taskService.UpdateCompletion(uint(completionID), user.ID, &req) + response, err := h.taskService.UpdateCompletion(c.Request().Context(), uint(completionID), user.ID, &req) if err != nil { return err } @@ -509,7 +509,7 @@ func (h *TaskHandler) DeleteCompletion(c echo.Context) error { return apperrors.BadRequest("error.invalid_completion_id") } - response, err := h.taskService.DeleteCompletion(uint(completionID), user.ID) + response, err := h.taskService.DeleteCompletion(c.Request().Context(), uint(completionID), user.ID) if err != nil { return err } @@ -520,7 +520,7 @@ func (h *TaskHandler) DeleteCompletion(c echo.Context) error { // GetCategories handles GET /api/tasks/categories/ func (h *TaskHandler) GetCategories(c echo.Context) error { - categories, err := h.taskService.GetCategories() + categories, err := h.taskService.GetCategories(c.Request().Context()) if err != nil { return err } @@ -529,7 +529,7 @@ func (h *TaskHandler) GetCategories(c echo.Context) error { // GetPriorities handles GET /api/tasks/priorities/ func (h *TaskHandler) GetPriorities(c echo.Context) error { - priorities, err := h.taskService.GetPriorities() + priorities, err := h.taskService.GetPriorities(c.Request().Context()) if err != nil { return err } @@ -538,7 +538,7 @@ func (h *TaskHandler) GetPriorities(c echo.Context) error { // GetFrequencies handles GET /api/tasks/frequencies/ func (h *TaskHandler) GetFrequencies(c echo.Context) error { - frequencies, err := h.taskService.GetFrequencies() + frequencies, err := h.taskService.GetFrequencies(c.Request().Context()) if err != nil { return err } diff --git a/internal/services/residence_service.go b/internal/services/residence_service.go index d7bfe96..385ab4c 100644 --- a/internal/services/residence_service.go +++ b/internal/services/residence_service.go @@ -1,6 +1,7 @@ package services import ( + "context" "errors" "time" @@ -60,9 +61,9 @@ func (s *ResidenceService) SetSubscriptionService(subService *SubscriptionServic // GetResidence gets a residence by ID with access check. // The `now` parameter is used for timezone-aware completion summary aggregation. -func (s *ResidenceService) GetResidence(residenceID, userID uint, now time.Time) (*responses.ResidenceResponse, error) { +func (s *ResidenceService) GetResidence(ctx context.Context, residenceID, userID uint, now time.Time) (*responses.ResidenceResponse, error) { // Check access - hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -70,7 +71,7 @@ func (s *ResidenceService) GetResidence(residenceID, userID uint, now time.Time) return nil, apperrors.Forbidden("error.residence_access_denied") } - residence, err := s.residenceRepo.FindByID(residenceID) + residence, err := s.residenceRepo.WithContext(ctx).FindByID(residenceID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.residence_not_found") @@ -82,7 +83,7 @@ func (s *ResidenceService) GetResidence(residenceID, userID uint, now time.Time) // Attach completion summary (honeycomb grid data) if s.taskRepo != nil { - summary, err := s.taskRepo.GetCompletionSummary(residenceID, now, 10) + summary, err := s.taskRepo.WithContext(ctx).GetCompletionSummary(residenceID, now, 10) if err != nil { log.Warn().Err(err).Uint("residence_id", residenceID).Msg("Failed to fetch completion summary") } else { @@ -94,8 +95,8 @@ func (s *ResidenceService) GetResidence(residenceID, userID uint, now time.Time) } // ListResidences lists all residences accessible to a user -func (s *ResidenceService) ListResidences(userID uint) ([]responses.ResidenceResponse, error) { - residences, err := s.residenceRepo.FindByUser(userID) +func (s *ResidenceService) ListResidences(ctx context.Context, userID uint) ([]responses.ResidenceResponse, error) { + residences, err := s.residenceRepo.WithContext(ctx).FindByUser(userID) if err != nil { return nil, apperrors.Internal(err) } @@ -109,8 +110,8 @@ func (s *ResidenceService) ListResidences(userID uint) ([]responses.ResidenceRes // // NOTE: Summary statistics (TotalTasks, TotalOverdue, etc.) are calculated client-side // from kanban data for performance. Only per-residence OverdueCount is returned from the server. -func (s *ResidenceService) GetMyResidences(userID uint, now time.Time) (*responses.MyResidencesResponse, error) { - residences, err := s.residenceRepo.FindByUser(userID) +func (s *ResidenceService) GetMyResidences(ctx context.Context, userID uint, now time.Time) (*responses.MyResidencesResponse, error) { + residences, err := s.residenceRepo.WithContext(ctx).FindByUser(userID) if err != nil { return nil, apperrors.Internal(err) } @@ -124,7 +125,7 @@ func (s *ResidenceService) GetMyResidences(userID uint, now time.Time) (*respons residenceIDs[i] = r.ID } - overdueCounts, err := s.taskRepo.GetOverdueCountByResidence(residenceIDs, now) + overdueCounts, err := s.taskRepo.WithContext(ctx).GetOverdueCountByResidence(residenceIDs, now) if err == nil && overdueCounts != nil { for i := range residenceResponses { if count, ok := overdueCounts[residenceResponses[i].ID]; ok { @@ -134,7 +135,7 @@ func (s *ResidenceService) GetMyResidences(userID uint, now time.Time) (*respons } // P-01: Batch fetch completion summaries in 2 queries total instead of 2*N - summaries, err := s.taskRepo.GetBatchCompletionSummaries(residenceIDs, now, 10) + summaries, err := s.taskRepo.WithContext(ctx).GetBatchCompletionSummaries(residenceIDs, now, 10) if err != nil { log.Warn().Err(err).Msg("Failed to fetch batch completion summaries") } else { @@ -157,9 +158,9 @@ func (s *ResidenceService) GetMyResidences(userID uint, now time.Time) (*respons // DEPRECATED: Summary statistics are now calculated client-side from kanban data. // This endpoint only returns TotalResidences; other fields will be zero. // Clients should use calculateSummaryFromKanban() instead. -func (s *ResidenceService) GetSummary(userID uint, now time.Time) (*responses.TotalSummary, error) { +func (s *ResidenceService) GetSummary(ctx context.Context, userID uint, now time.Time) (*responses.TotalSummary, error) { // Get residence IDs (lightweight - no preloads) - residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID) + residenceIDs, err := s.residenceRepo.WithContext(ctx).FindResidenceIDsByUser(userID) if err != nil { return nil, apperrors.Internal(err) } @@ -182,7 +183,7 @@ func (s *ResidenceService) getSummaryForUser(_ uint) responses.TotalSummary { } // CreateResidence creates a new residence and returns it with updated summary -func (s *ResidenceService) CreateResidence(req *requests.CreateResidenceRequest, ownerID uint) (*responses.ResidenceWithSummaryResponse, error) { +func (s *ResidenceService) CreateResidence(ctx context.Context, req *requests.CreateResidenceRequest, ownerID uint) (*responses.ResidenceWithSummaryResponse, error) { // Check subscription tier limits (if subscription service is wired up) if s.subscriptionService != nil { if err := s.subscriptionService.CheckLimit(ownerID, "properties"); err != nil { @@ -253,12 +254,12 @@ func (s *ResidenceService) CreateResidence(req *requests.CreateResidenceRequest, residence.HasAttic = *req.HasAttic } - if err := s.residenceRepo.Create(residence); err != nil { + if err := s.residenceRepo.WithContext(ctx).Create(residence); err != nil { return nil, apperrors.Internal(err) } // Reload with relations - residence, err := s.residenceRepo.FindByID(residence.ID) + residence, err := s.residenceRepo.WithContext(ctx).FindByID(residence.ID) if err != nil { return nil, apperrors.Internal(err) } @@ -273,9 +274,9 @@ func (s *ResidenceService) CreateResidence(req *requests.CreateResidenceRequest, } // UpdateResidence updates a residence and returns it with updated summary -func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *requests.UpdateResidenceRequest) (*responses.ResidenceWithSummaryResponse, error) { +func (s *ResidenceService) UpdateResidence(ctx context.Context, residenceID, userID uint, req *requests.UpdateResidenceRequest) (*responses.ResidenceWithSummaryResponse, error) { // Check ownership - isOwner, err := s.residenceRepo.IsOwner(residenceID, userID) + isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -283,7 +284,7 @@ func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *reques return nil, apperrors.Forbidden("error.not_residence_owner") } - residence, err := s.residenceRepo.FindByID(residenceID) + residence, err := s.residenceRepo.WithContext(ctx).FindByID(residenceID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.residence_not_found") @@ -388,12 +389,12 @@ func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *reques residence.LandscapingType = req.LandscapingType } - if err := s.residenceRepo.Update(residence); err != nil { + if err := s.residenceRepo.WithContext(ctx).Update(residence); err != nil { return nil, apperrors.Internal(err) } // Reload with relations - residence, err = s.residenceRepo.FindByID(residence.ID) + residence, err = s.residenceRepo.WithContext(ctx).FindByID(residence.ID) if err != nil { return nil, apperrors.Internal(err) } @@ -408,9 +409,9 @@ func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *reques } // DeleteResidence soft-deletes a residence and returns updated summary -func (s *ResidenceService) DeleteResidence(residenceID, userID uint) (*responses.ResidenceDeleteWithSummaryResponse, error) { +func (s *ResidenceService) DeleteResidence(ctx context.Context, residenceID, userID uint) (*responses.ResidenceDeleteWithSummaryResponse, error) { // Check ownership - isOwner, err := s.residenceRepo.IsOwner(residenceID, userID) + isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -418,7 +419,7 @@ func (s *ResidenceService) DeleteResidence(residenceID, userID uint) (*responses return nil, apperrors.Forbidden("error.not_residence_owner") } - if err := s.residenceRepo.Delete(residenceID); err != nil { + if err := s.residenceRepo.WithContext(ctx).Delete(residenceID); err != nil { return nil, apperrors.Internal(err) } @@ -432,9 +433,9 @@ func (s *ResidenceService) DeleteResidence(residenceID, userID uint) (*responses } // GenerateShareCode generates a new share code for a residence -func (s *ResidenceService) GenerateShareCode(residenceID, userID uint, expiresInHours int) (*responses.GenerateShareCodeResponse, error) { +func (s *ResidenceService) GenerateShareCode(ctx context.Context, residenceID, userID uint, expiresInHours int) (*responses.GenerateShareCodeResponse, error) { // Check ownership - isOwner, err := s.residenceRepo.IsOwner(residenceID, userID) + isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -447,7 +448,7 @@ func (s *ResidenceService) GenerateShareCode(residenceID, userID uint, expiresIn expiresInHours = 24 } - shareCode, err := s.residenceRepo.CreateShareCode(residenceID, userID, time.Duration(expiresInHours)*time.Hour) + shareCode, err := s.residenceRepo.WithContext(ctx).CreateShareCode(residenceID, userID, time.Duration(expiresInHours)*time.Hour) if err != nil { return nil, apperrors.Internal(err) } @@ -459,9 +460,9 @@ func (s *ResidenceService) GenerateShareCode(residenceID, userID uint, expiresIn } // GetShareCode retrieves the active share code for a residence (if any) -func (s *ResidenceService) GetShareCode(residenceID, userID uint) (*responses.ShareCodeResponse, error) { +func (s *ResidenceService) GetShareCode(ctx context.Context, residenceID, userID uint) (*responses.ShareCodeResponse, error) { // Check access - hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -469,7 +470,7 @@ func (s *ResidenceService) GetShareCode(residenceID, userID uint) (*responses.Sh return nil, apperrors.Forbidden("error.residence_access_denied") } - shareCode, err := s.residenceRepo.GetActiveShareCode(residenceID) + shareCode, err := s.residenceRepo.WithContext(ctx).GetActiveShareCode(residenceID) if err != nil { return nil, apperrors.Internal(err) } @@ -482,9 +483,9 @@ func (s *ResidenceService) GetShareCode(residenceID, userID uint) (*responses.Sh } // GenerateSharePackage generates a share code and returns package metadata for .honeydue file -func (s *ResidenceService) GenerateSharePackage(residenceID, userID uint, expiresInHours int) (*responses.SharePackageResponse, error) { +func (s *ResidenceService) GenerateSharePackage(ctx context.Context, residenceID, userID uint, expiresInHours int) (*responses.SharePackageResponse, error) { // Check ownership (only owners can share residences) - isOwner, err := s.residenceRepo.IsOwner(residenceID, userID) + isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -493,13 +494,13 @@ func (s *ResidenceService) GenerateSharePackage(residenceID, userID uint, expire } // Get residence details for the package - residence, err := s.residenceRepo.FindByID(residenceID) + residence, err := s.residenceRepo.WithContext(ctx).FindByID(residenceID) if err != nil { return nil, apperrors.Internal(err) } // Get the user who's sharing - user, err := s.userRepo.FindByID(userID) + user, err := s.userRepo.WithContext(ctx).FindByID(userID) if err != nil { return nil, apperrors.Internal(err) } @@ -510,7 +511,7 @@ func (s *ResidenceService) GenerateSharePackage(residenceID, userID uint, expire } // Generate the share code - shareCode, err := s.residenceRepo.CreateShareCode(residenceID, userID, time.Duration(expiresInHours)*time.Hour) + shareCode, err := s.residenceRepo.WithContext(ctx).CreateShareCode(residenceID, userID, time.Duration(expiresInHours)*time.Hour) if err != nil { return nil, apperrors.Internal(err) } @@ -524,9 +525,9 @@ func (s *ResidenceService) GenerateSharePackage(residenceID, userID uint, expire } // JoinWithCode allows a user to join a residence using a share code -func (s *ResidenceService) JoinWithCode(code string, userID uint) (*responses.JoinResidenceResponse, error) { +func (s *ResidenceService) JoinWithCode(ctx context.Context, code string, userID uint) (*responses.JoinResidenceResponse, error) { // Find the share code - shareCode, err := s.residenceRepo.FindShareCodeByCode(code) + shareCode, err := s.residenceRepo.WithContext(ctx).FindShareCodeByCode(code) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.share_code_invalid") @@ -535,7 +536,7 @@ func (s *ResidenceService) JoinWithCode(code string, userID uint) (*responses.Jo } // Check if already a member - hasAccess, err := s.residenceRepo.HasAccess(shareCode.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(shareCode.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -544,19 +545,19 @@ func (s *ResidenceService) JoinWithCode(code string, userID uint) (*responses.Jo } // Add user to residence - if err := s.residenceRepo.AddUser(shareCode.ResidenceID, userID); err != nil { + if err := s.residenceRepo.WithContext(ctx).AddUser(shareCode.ResidenceID, userID); err != nil { return nil, apperrors.Internal(err) } // Mark share code as used (one-time use) - if err := s.residenceRepo.DeactivateShareCode(shareCode.ID); err != nil { + if err := s.residenceRepo.WithContext(ctx).DeactivateShareCode(shareCode.ID); err != nil { // Log the error but don't fail the join - the user has already been added // The code will just be usable by others until it expires log.Error().Err(err).Uint("code_id", shareCode.ID).Msg("Failed to deactivate share code after join") } // Get the residence with full details - residence, err := s.residenceRepo.FindByID(shareCode.ResidenceID) + residence, err := s.residenceRepo.WithContext(ctx).FindByID(shareCode.ResidenceID) if err != nil { return nil, apperrors.Internal(err) } @@ -572,9 +573,9 @@ func (s *ResidenceService) JoinWithCode(code string, userID uint) (*responses.Jo } // GetResidenceUsers returns all users with access to a residence -func (s *ResidenceService) GetResidenceUsers(residenceID, userID uint) ([]responses.ResidenceUserResponse, error) { +func (s *ResidenceService) GetResidenceUsers(ctx context.Context, residenceID, userID uint) ([]responses.ResidenceUserResponse, error) { // Check access - hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -582,7 +583,7 @@ func (s *ResidenceService) GetResidenceUsers(residenceID, userID uint) ([]respon return nil, apperrors.Forbidden("error.residence_access_denied") } - users, err := s.residenceRepo.GetResidenceUsers(residenceID) + users, err := s.residenceRepo.WithContext(ctx).GetResidenceUsers(residenceID) if err != nil { return nil, apperrors.Internal(err) } @@ -596,9 +597,9 @@ func (s *ResidenceService) GetResidenceUsers(residenceID, userID uint) ([]respon } // RemoveUser removes a user from a residence (owner only) -func (s *ResidenceService) RemoveUser(residenceID, userIDToRemove, requestingUserID uint) error { +func (s *ResidenceService) RemoveUser(ctx context.Context, residenceID, userIDToRemove, requestingUserID uint) error { // Check ownership - isOwner, err := s.residenceRepo.IsOwner(residenceID, requestingUserID) + isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, requestingUserID) if err != nil { return apperrors.Internal(err) } @@ -612,7 +613,7 @@ func (s *ResidenceService) RemoveUser(residenceID, userIDToRemove, requestingUse } // Check if the residence exists - residence, err := s.residenceRepo.FindByIDSimple(residenceID) + residence, err := s.residenceRepo.WithContext(ctx).FindByIDSimple(residenceID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return apperrors.NotFound("error.residence_not_found") @@ -625,7 +626,7 @@ func (s *ResidenceService) RemoveUser(residenceID, userIDToRemove, requestingUse return apperrors.BadRequest("error.cannot_remove_owner") } - if err := s.residenceRepo.RemoveUser(residenceID, userIDToRemove); err != nil { + if err := s.residenceRepo.WithContext(ctx).RemoveUser(residenceID, userIDToRemove); err != nil { return apperrors.Internal(err) } @@ -633,8 +634,8 @@ func (s *ResidenceService) RemoveUser(residenceID, userIDToRemove, requestingUse } // GetResidenceTypes returns all residence types -func (s *ResidenceService) GetResidenceTypes() ([]responses.ResidenceTypeResponse, error) { - types, err := s.residenceRepo.GetAllResidenceTypes() +func (s *ResidenceService) GetResidenceTypes(ctx context.Context) ([]responses.ResidenceTypeResponse, error) { + types, err := s.residenceRepo.WithContext(ctx).GetAllResidenceTypes() if err != nil { return nil, apperrors.Internal(err) } @@ -674,9 +675,9 @@ type TasksReportResponse struct { } // GenerateTasksReport generates a report of all tasks for a residence -func (s *ResidenceService) GenerateTasksReport(residenceID, userID uint) (*TasksReportResponse, error) { +func (s *ResidenceService) GenerateTasksReport(ctx context.Context, residenceID, userID uint) (*TasksReportResponse, error) { // Check access - hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -685,7 +686,7 @@ func (s *ResidenceService) GenerateTasksReport(residenceID, userID uint) (*Tasks } // Get residence details - residence, err := s.residenceRepo.FindByIDSimple(residenceID) + residence, err := s.residenceRepo.WithContext(ctx).FindByIDSimple(residenceID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.residence_not_found") @@ -694,7 +695,7 @@ func (s *ResidenceService) GenerateTasksReport(residenceID, userID uint) (*Tasks } // Get all tasks for the residence - tasks, err := s.residenceRepo.GetTasksForReport(residenceID) + tasks, err := s.residenceRepo.WithContext(ctx).GetTasksForReport(residenceID) if err != nil { return nil, apperrors.Internal(err) } diff --git a/internal/services/residence_service_test.go b/internal/services/residence_service_test.go index 399ec23..41e2a89 100644 --- a/internal/services/residence_service_test.go +++ b/internal/services/residence_service_test.go @@ -1,6 +1,7 @@ package services import ( + "context" "fmt" "net/http" "testing" @@ -44,7 +45,7 @@ func TestResidenceService_CreateResidence(t *testing.T) { PostalCode: "78701", } - resp, err := service.CreateResidence(req, user.ID) + resp, err := service.CreateResidence(context.Background(), req, user.ID) require.NoError(t, err) assert.NotNil(t, resp) assert.Equal(t, "Test House", resp.Data.Name) @@ -82,7 +83,7 @@ func TestResidenceService_CreateResidence_WithOptionalFields(t *testing.T) { IsPrimary: &isPrimary, } - resp, err := service.CreateResidence(req, user.ID) + resp, err := service.CreateResidence(context.Background(), req, user.ID) require.NoError(t, err) assert.Equal(t, "Canada", resp.Data.Country) assert.Equal(t, 3, *resp.Data.Bedrooms) @@ -102,7 +103,7 @@ func TestResidenceService_GetResidence(t *testing.T) { user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") - resp, err := service.GetResidence(residence.ID, user.ID, time.Now()) + resp, err := service.GetResidence(context.Background(), residence.ID, user.ID, time.Now()) require.NoError(t, err) assert.Equal(t, residence.ID, resp.ID) assert.Equal(t, "Test House", resp.Name) @@ -119,7 +120,7 @@ func TestResidenceService_GetResidence_AccessDenied(t *testing.T) { otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") - _, err := service.GetResidence(residence.ID, otherUser.ID, time.Now()) + _, err := service.GetResidence(context.Background(), residence.ID, otherUser.ID, time.Now()) testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") } @@ -132,7 +133,7 @@ func TestResidenceService_GetResidence_NotFound(t *testing.T) { user := testutil.CreateTestUser(t, db, "user", "user@test.com", "password") - _, err := service.GetResidence(9999, user.ID, time.Now()) + _, err := service.GetResidence(context.Background(), 9999, user.ID, time.Now()) assert.Error(t, err) } @@ -147,7 +148,7 @@ func TestResidenceService_ListResidences(t *testing.T) { testutil.CreateTestResidence(t, db, user.ID, "House 1") testutil.CreateTestResidence(t, db, user.ID, "House 2") - resp, err := service.ListResidences(user.ID) + resp, err := service.ListResidences(context.Background(), user.ID) require.NoError(t, err) assert.Len(t, resp, 2) } @@ -169,7 +170,7 @@ func TestResidenceService_UpdateResidence(t *testing.T) { City: &newCity, } - resp, err := service.UpdateResidence(residence.ID, user.ID, req) + resp, err := service.UpdateResidence(context.Background(), residence.ID, user.ID, req) require.NoError(t, err) assert.Equal(t, "Updated Name", resp.Data.Name) assert.Equal(t, "Dallas", resp.Data.City) @@ -192,7 +193,7 @@ func TestResidenceService_UpdateResidence_NotOwner(t *testing.T) { newName := "Updated" req := &requests.UpdateResidenceRequest{Name: &newName} - _, err := service.UpdateResidence(residence.ID, sharedUser.ID, req) + _, err := service.UpdateResidence(context.Background(), residence.ID, sharedUser.ID, req) testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner") } @@ -206,11 +207,11 @@ func TestResidenceService_DeleteResidence(t *testing.T) { user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") - _, err := service.DeleteResidence(residence.ID, user.ID) + _, err := service.DeleteResidence(context.Background(), residence.ID, user.ID) require.NoError(t, err) // Should not be found - _, err = service.GetResidence(residence.ID, user.ID, time.Now()) + _, err = service.GetResidence(context.Background(), residence.ID, user.ID, time.Now()) assert.Error(t, err) } @@ -226,7 +227,7 @@ func TestResidenceService_DeleteResidence_NotOwner(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") residenceRepo.AddUser(residence.ID, sharedUser.ID) - _, err := service.DeleteResidence(residence.ID, sharedUser.ID) + _, err := service.DeleteResidence(context.Background(), residence.ID, sharedUser.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner") } @@ -240,7 +241,7 @@ func TestResidenceService_GenerateShareCode(t *testing.T) { user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") - resp, err := service.GenerateShareCode(residence.ID, user.ID, 24) + resp, err := service.GenerateShareCode(context.Background(), residence.ID, user.ID, 24) require.NoError(t, err) assert.NotEmpty(t, resp.ShareCode.Code) assert.Len(t, resp.ShareCode.Code, 6) @@ -258,11 +259,11 @@ func TestResidenceService_JoinWithCode(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") // Generate share code - shareResp, err := service.GenerateShareCode(residence.ID, owner.ID, 24) + shareResp, err := service.GenerateShareCode(context.Background(), residence.ID, owner.ID, 24) require.NoError(t, err) // Join with code - joinResp, err := service.JoinWithCode(shareResp.ShareCode.Code, newUser.ID) + joinResp, err := service.JoinWithCode(context.Background(), shareResp.ShareCode.Code, newUser.ID) require.NoError(t, err) assert.Equal(t, residence.ID, joinResp.Residence.ID) @@ -281,10 +282,10 @@ func TestResidenceService_JoinWithCode_AlreadyMember(t *testing.T) { owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") - shareResp, _ := service.GenerateShareCode(residence.ID, owner.ID, 24) + shareResp, _ := service.GenerateShareCode(context.Background(), residence.ID, owner.ID, 24) // Owner tries to join their own residence - _, err := service.JoinWithCode(shareResp.ShareCode.Code, owner.ID) + _, err := service.JoinWithCode(context.Background(), shareResp.ShareCode.Code, owner.ID) testutil.AssertAppError(t, err, http.StatusConflict, "error.user_already_member") } @@ -300,7 +301,7 @@ func TestResidenceService_GetResidenceUsers(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") residenceRepo.AddUser(residence.ID, user1.ID) - users, err := service.GetResidenceUsers(residence.ID, owner.ID) + users, err := service.GetResidenceUsers(context.Background(), residence.ID, owner.ID) require.NoError(t, err) assert.Len(t, users, 2) // owner + shared user } @@ -317,7 +318,7 @@ func TestResidenceService_RemoveUser(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") residenceRepo.AddUser(residence.ID, sharedUser.ID) - err := service.RemoveUser(residence.ID, sharedUser.ID, owner.ID) + err := service.RemoveUser(context.Background(), residence.ID, sharedUser.ID, owner.ID) require.NoError(t, err) hasAccess, _ := residenceRepo.HasAccess(residence.ID, sharedUser.ID) @@ -334,7 +335,7 @@ func TestResidenceService_RemoveUser_CannotRemoveOwner(t *testing.T) { owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") - err := service.RemoveUser(residence.ID, owner.ID, owner.ID) + err := service.RemoveUser(context.Background(), residence.ID, owner.ID, owner.ID) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.cannot_remove_owner") } @@ -389,7 +390,7 @@ func TestCreateResidence_FreeTier_EnforcesLimit(t *testing.T) { StateProvince: "TX", PostalCode: "78701", } - resp, err := service.CreateResidence(req, owner.ID) + resp, err := service.CreateResidence(context.Background(), req, owner.ID) require.NoError(t, err) assert.Equal(t, "First House", resp.Data.Name) @@ -401,7 +402,7 @@ func TestCreateResidence_FreeTier_EnforcesLimit(t *testing.T) { StateProvince: "TX", PostalCode: "78702", } - _, err = service.CreateResidence(req2, owner.ID) + _, err = service.CreateResidence(context.Background(), req2, owner.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.properties_limit_exceeded") } @@ -446,7 +447,7 @@ func TestCreateResidence_ProTier_AllowsMore(t *testing.T) { StateProvince: "TX", PostalCode: "78701", } - resp, err := service.CreateResidence(req, owner.ID) + resp, err := service.CreateResidence(context.Background(), req, owner.ID) require.NoError(t, err, "Pro user should be able to create residence %d", i) assert.Equal(t, fmt.Sprintf("House %d", i), resp.Data.Name) } @@ -470,7 +471,7 @@ func TestResidenceService_GetMyResidences(t *testing.T) { testutil.CreateTestResidence(t, db, user.ID, "House 1") testutil.CreateTestResidence(t, db, user.ID, "House 2") - resp, err := service.GetMyResidences(user.ID, time.Now()) + resp, err := service.GetMyResidences(context.Background(), user.ID, time.Now()) require.NoError(t, err) assert.Len(t, resp.Residences, 2) } @@ -484,7 +485,7 @@ func TestResidenceService_GetMyResidences_NoResidences(t *testing.T) { user := testutil.CreateTestUser(t, db, "loner", "loner@test.com", "Password123") - resp, err := service.GetMyResidences(user.ID, time.Now()) + resp, err := service.GetMyResidences(context.Background(), user.ID, time.Now()) require.NoError(t, err) assert.Empty(t, resp.Residences) } @@ -502,7 +503,7 @@ func TestResidenceService_GetSummary(t *testing.T) { testutil.CreateTestResidence(t, db, user.ID, "House 1") testutil.CreateTestResidence(t, db, user.ID, "House 2") - resp, err := service.GetSummary(user.ID, time.Now()) + resp, err := service.GetSummary(context.Background(), user.ID, time.Now()) require.NoError(t, err) assert.Equal(t, 2, resp.TotalResidences) } @@ -516,7 +517,7 @@ func TestResidenceService_GetSummary_NoResidences(t *testing.T) { user := testutil.CreateTestUser(t, db, "loner", "loner@test.com", "Password123") - resp, err := service.GetSummary(user.ID, time.Now()) + resp, err := service.GetSummary(context.Background(), user.ID, time.Now()) require.NoError(t, err) assert.Equal(t, 0, resp.TotalResidences) } @@ -533,7 +534,7 @@ func TestResidenceService_GetShareCode_NoActiveCode(t *testing.T) { user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") - resp, err := service.GetShareCode(residence.ID, user.ID) + resp, err := service.GetShareCode(context.Background(), residence.ID, user.ID) require.NoError(t, err) assert.Nil(t, resp) // No active code } @@ -549,7 +550,7 @@ func TestResidenceService_GetShareCode_AccessDenied(t *testing.T) { other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") - _, err := service.GetShareCode(residence.ID, other.ID) + _, err := service.GetShareCode(context.Background(), residence.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") } @@ -567,7 +568,7 @@ func TestResidenceService_GenerateShareCode_NotOwner(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") residenceRepo.AddUser(residence.ID, shared.ID) - _, err := service.GenerateShareCode(residence.ID, shared.ID, 24) + _, err := service.GenerateShareCode(context.Background(), residence.ID, shared.ID, 24) testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner") } @@ -582,7 +583,7 @@ func TestResidenceService_GenerateShareCode_DefaultExpiry(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") // Pass 0 hours — should default to 24 - resp, err := service.GenerateShareCode(residence.ID, user.ID, 0) + resp, err := service.GenerateShareCode(context.Background(), residence.ID, user.ID, 0) require.NoError(t, err) assert.NotEmpty(t, resp.ShareCode.Code) } @@ -599,7 +600,7 @@ func TestResidenceService_GenerateSharePackage(t *testing.T) { user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") - resp, err := service.GenerateSharePackage(residence.ID, user.ID, 48) + resp, err := service.GenerateSharePackage(context.Background(), residence.ID, user.ID, 48) require.NoError(t, err) assert.NotEmpty(t, resp.ShareCode) assert.Equal(t, "Test House", resp.ResidenceName) @@ -618,7 +619,7 @@ func TestResidenceService_GenerateSharePackage_NotOwner(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") residenceRepo.AddUser(residence.ID, shared.ID) - _, err := service.GenerateSharePackage(residence.ID, shared.ID, 24) + _, err := service.GenerateSharePackage(context.Background(), residence.ID, shared.ID, 24) testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner") } @@ -633,7 +634,7 @@ func TestResidenceService_JoinWithCode_InvalidCode(t *testing.T) { user := testutil.CreateTestUser(t, db, "user", "user@test.com", "Password123") - _, err := service.JoinWithCode("BADCODE", user.ID) + _, err := service.JoinWithCode(context.Background(), "BADCODE", user.ID) testutil.AssertAppError(t, err, http.StatusNotFound, "error.share_code_invalid") } @@ -653,7 +654,7 @@ func TestResidenceService_RemoveUser_NotOwner(t *testing.T) { residenceRepo.AddUser(residence.ID, shared.ID) // shared user tries to remove other — should fail because shared is not owner - err := service.RemoveUser(residence.ID, other.ID, shared.ID) + err := service.RemoveUser(context.Background(), residence.ID, other.ID, shared.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.not_residence_owner") } @@ -670,7 +671,7 @@ func TestResidenceService_GetResidenceUsers_AccessDenied(t *testing.T) { other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") - _, err := service.GetResidenceUsers(residence.ID, other.ID) + _, err := service.GetResidenceUsers(context.Background(), residence.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") } @@ -684,7 +685,7 @@ func TestResidenceService_GetResidenceTypes(t *testing.T) { cfg := &config.Config{} service := NewResidenceService(residenceRepo, userRepo, cfg) - resp, err := service.GetResidenceTypes() + resp, err := service.GetResidenceTypes(context.Background()) require.NoError(t, err) // SeedLookupData creates 4 residence types assert.Len(t, resp, 4) @@ -711,7 +712,7 @@ func TestResidenceService_UpdateResidence_HomeProfileFields(t *testing.T) { HeatingType: &heatingType, } - resp, err := service.UpdateResidence(residence.ID, user.ID, req) + resp, err := service.UpdateResidence(context.Background(), residence.ID, user.ID, req) require.NoError(t, err) assert.True(t, resp.Data.HasPool) assert.True(t, resp.Data.HasGarage) @@ -740,7 +741,7 @@ func TestResidenceService_CreateResidence_HomeProfileFields(t *testing.T) { HasSeptic: &hasSeptic, } - resp, err := service.CreateResidence(req, user.ID) + resp, err := service.CreateResidence(context.Background(), req, user.ID) require.NoError(t, err) assert.True(t, resp.Data.HasPool) assert.True(t, resp.Data.HasSeptic) @@ -760,7 +761,7 @@ func TestResidenceService_GetResidence_SharedUser(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") residenceRepo.AddUser(residence.ID, shared.ID) - resp, err := service.GetResidence(residence.ID, shared.ID, time.Now()) + resp, err := service.GetResidence(context.Background(), residence.ID, shared.ID, time.Now()) require.NoError(t, err) assert.Equal(t, "Test House", resp.Name) } @@ -780,7 +781,7 @@ func TestResidenceService_GetMyResidences_WithTaskRepo(t *testing.T) { testutil.CreateTestResidence(t, db, user.ID, "House 1") testutil.CreateTestResidence(t, db, user.ID, "House 2") - resp, err := service.GetMyResidences(user.ID, time.Now()) + resp, err := service.GetMyResidences(context.Background(), user.ID, time.Now()) require.NoError(t, err) assert.Len(t, resp.Residences, 2) } @@ -799,7 +800,7 @@ func TestResidenceService_GetResidence_WithTaskRepo(t *testing.T) { user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") - resp, err := service.GetResidence(residence.ID, user.ID, time.Now()) + resp, err := service.GetResidence(context.Background(), residence.ID, user.ID, time.Now()) require.NoError(t, err) assert.Equal(t, "Test House", resp.Name) } @@ -816,7 +817,7 @@ func TestResidenceService_GenerateShareCode_NegativeExpiry(t *testing.T) { user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") - resp, err := service.GenerateShareCode(residence.ID, user.ID, -5) + resp, err := service.GenerateShareCode(context.Background(), residence.ID, user.ID, -5) require.NoError(t, err) assert.NotEmpty(t, resp.ShareCode.Code) } @@ -834,7 +835,7 @@ func TestResidenceService_GenerateSharePackage_DefaultExpiry(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") // Pass 0 hours — should default to 24 - resp, err := service.GenerateSharePackage(residence.ID, user.ID, 0) + resp, err := service.GenerateSharePackage(context.Background(), residence.ID, user.ID, 0) require.NoError(t, err) assert.NotEmpty(t, resp.ShareCode) assert.Equal(t, "Test House", resp.ResidenceName) @@ -856,7 +857,7 @@ func TestResidenceService_RemoveUser_OwnerViaResidenceOwnerID(t *testing.T) { // Try removing the owner (by residence.OwnerID) — even though requestingUserID != userIDToRemove // The second check (userIDToRemove == residence.OwnerID) should catch this - err := service.RemoveUser(residence.ID, owner.ID, owner.ID) + err := service.RemoveUser(context.Background(), residence.ID, owner.ID, owner.ID) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.cannot_remove_owner") } @@ -877,7 +878,7 @@ func TestResidenceService_GenerateTasksReport(t *testing.T) { testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 1") testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 2") - report, err := service.GenerateTasksReport(residence.ID, user.ID) + report, err := service.GenerateTasksReport(context.Background(), residence.ID, user.ID) require.NoError(t, err) assert.Equal(t, residence.ID, report.ResidenceID) assert.Equal(t, "Test House", report.ResidenceName) @@ -895,7 +896,7 @@ func TestResidenceService_GenerateTasksReport_AccessDenied(t *testing.T) { other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") - _, err := service.GenerateTasksReport(residence.ID, other.ID) + _, err := service.GenerateTasksReport(context.Background(), residence.ID, other.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") } @@ -909,7 +910,7 @@ func TestResidenceService_GenerateTasksReport_NotFound(t *testing.T) { user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Non-existent residence — user has no access - _, err := service.GenerateTasksReport(9999, user.ID) + _, err := service.GenerateTasksReport(context.Background(), 9999, user.ID) assert.Error(t, err) } @@ -926,11 +927,11 @@ func TestResidenceService_GetShareCode_WithActiveCode(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") // Generate a share code first - _, err := service.GenerateShareCode(residence.ID, user.ID, 24) + _, err := service.GenerateShareCode(context.Background(), residence.ID, user.ID, 24) require.NoError(t, err) // Now get the active code - resp, err := service.GetShareCode(residence.ID, user.ID) + resp, err := service.GetShareCode(context.Background(), residence.ID, user.ID) require.NoError(t, err) assert.NotNil(t, resp) assert.NotEmpty(t, resp.Code) @@ -970,7 +971,7 @@ func TestResidenceService_CreateResidence_AllBooleanFields(t *testing.T) { HasAttic: &hasAttic, } - resp, err := service.CreateResidence(req, user.ID) + resp, err := service.CreateResidence(context.Background(), req, user.ID) require.NoError(t, err) assert.True(t, resp.Data.HasPool) assert.True(t, resp.Data.HasSprinklerSystem) @@ -1047,7 +1048,7 @@ func TestResidenceService_UpdateResidence_AllOptionalFields(t *testing.T) { LandscapingType: &landscapingType, } - resp, err := service.UpdateResidence(residence.ID, user.ID, req) + resp, err := service.UpdateResidence(context.Background(), residence.ID, user.ID, req) require.NoError(t, err) assert.Equal(t, "456 New St", resp.Data.StreetAddress) assert.Equal(t, "CA", resp.Data.StateProvince) @@ -1067,7 +1068,7 @@ func TestResidenceService_ListResidences_NoResidences(t *testing.T) { user := testutil.CreateTestUser(t, db, "loner", "loner@test.com", "Password123") - resp, err := service.ListResidences(user.ID) + resp, err := service.ListResidences(context.Background(), user.ID) require.NoError(t, err) assert.Empty(t, resp) } diff --git a/internal/services/task_service.go b/internal/services/task_service.go index f32ffad..07a1a92 100644 --- a/internal/services/task_service.go +++ b/internal/services/task_service.go @@ -79,8 +79,8 @@ func (s *TaskService) getSummaryForUser(_ uint) responses.TotalSummary { // === Task CRUD === // GetTask gets a task by ID with access check -func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, error) { - task, err := s.taskRepo.FindByID(taskID) +func (s *TaskService) GetTask(ctx context.Context, taskID, userID uint) (*responses.TaskResponse, error) { + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -89,7 +89,7 @@ func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, err } // Check access via residence - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -164,9 +164,9 @@ func (s *TaskService) GetTasksByResidence(ctx context.Context, residenceID, user // CreateTask creates a new task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. -func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { +func (s *TaskService) CreateTask(ctx context.Context, req *requests.CreateTaskRequest, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { // Check residence access - hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(req.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -193,12 +193,12 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, n TaskTemplateID: req.TemplateID, } - if err := s.taskRepo.Create(task); err != nil { + if err := s.taskRepo.WithContext(ctx).Create(task); err != nil { return nil, apperrors.Internal(err) } // Reload with relations - task, err = s.taskRepo.FindByID(task.ID) + task, err = s.taskRepo.WithContext(ctx).FindByID(task.ID) if err != nil { return nil, apperrors.Internal(err) } @@ -216,13 +216,13 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, n // // `now` should be the start of day in the user's timezone for accurate // kanban column categorisation on the returned task list. -func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, userID uint, now time.Time) (*responses.BulkCreateTasksResponse, error) { +func (s *TaskService) BulkCreateTasks(ctx context.Context, req *requests.BulkCreateTasksRequest, userID uint, now time.Time) (*responses.BulkCreateTasksResponse, error) { if len(req.Tasks) == 0 { return nil, apperrors.BadRequest("error.task_list_empty") } // Check residence access once. - hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(req.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -232,7 +232,7 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user createdIDs := make([]uint, 0, len(req.Tasks)) - err = s.taskRepo.DB().Transaction(func(tx *gorm.DB) error { + err = s.taskRepo.WithContext(ctx).DB().Transaction(func(tx *gorm.DB) error { for i := range req.Tasks { entry := req.Tasks[i] // Force the residence ID to the batch-level value so clients @@ -257,7 +257,7 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user ContractorID: entry.ContractorID, TaskTemplateID: entry.TemplateID, } - if err := s.taskRepo.CreateTx(tx, task); err != nil { + if err := s.taskRepo.WithContext(ctx).CreateTx(tx, task); err != nil { return fmt.Errorf("create task %d of %d: %w", i+1, len(req.Tasks), err) } createdIDs = append(createdIDs, task.ID) @@ -272,7 +272,7 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user // happen outside the transaction — rows are already committed. created := make([]responses.TaskResponse, 0, len(createdIDs)) for _, id := range createdIDs { - t, ferr := s.taskRepo.FindByID(id) + t, ferr := s.taskRepo.WithContext(ctx).FindByID(id) if ferr != nil { return nil, apperrors.Internal(ferr) } @@ -288,8 +288,8 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user // UpdateTask updates a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. -func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest, now time.Time) (*responses.TaskWithSummaryResponse, error) { - task, err := s.taskRepo.FindByID(taskID) +func (s *TaskService) UpdateTask(ctx context.Context, taskID, userID uint, req *requests.UpdateTaskRequest, now time.Time) (*responses.TaskWithSummaryResponse, error) { + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -298,7 +298,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -349,7 +349,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe task.ContractorID = req.ContractorID } - if err := s.taskRepo.Update(task); err != nil { + if err := s.taskRepo.WithContext(ctx).Update(task); err != nil { if errors.Is(err, repositories.ErrVersionConflict) { return nil, apperrors.Conflict("error.version_conflict") } @@ -357,7 +357,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe } // Reload - task, err = s.taskRepo.FindByID(task.ID) + task, err = s.taskRepo.WithContext(ctx).FindByID(task.ID) if err != nil { return nil, apperrors.Internal(err) } @@ -369,8 +369,8 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe } // DeleteTask deletes a task -func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) { - task, err := s.taskRepo.FindByID(taskID) +func (s *TaskService) DeleteTask(ctx context.Context, taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) { + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -379,7 +379,7 @@ func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSumm } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -387,7 +387,7 @@ func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSumm return nil, apperrors.Forbidden("error.task_access_denied") } - if err := s.taskRepo.Delete(taskID); err != nil { + if err := s.taskRepo.WithContext(ctx).Delete(taskID); err != nil { return nil, apperrors.Internal(err) } @@ -401,8 +401,8 @@ func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSumm // MarkInProgress marks a task as in progress. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. -func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { - task, err := s.taskRepo.FindByID(taskID) +func (s *TaskService) MarkInProgress(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -411,7 +411,7 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -419,7 +419,7 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo return nil, apperrors.Forbidden("error.task_access_denied") } - if err := s.taskRepo.MarkInProgress(taskID, task.Version); err != nil { + if err := s.taskRepo.WithContext(ctx).MarkInProgress(taskID, task.Version); err != nil { if errors.Is(err, repositories.ErrVersionConflict) { return nil, apperrors.Conflict("error.version_conflict") } @@ -427,7 +427,7 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo } // Reload - task, err = s.taskRepo.FindByID(taskID) + task, err = s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { return nil, apperrors.Internal(err) } @@ -440,8 +440,8 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo // CancelTask cancels a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. -func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { - task, err := s.taskRepo.FindByID(taskID) +func (s *TaskService) CancelTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -450,7 +450,7 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -462,7 +462,7 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses return nil, apperrors.BadRequest("error.task_already_cancelled") } - if err := s.taskRepo.Cancel(taskID, task.Version); err != nil { + if err := s.taskRepo.WithContext(ctx).Cancel(taskID, task.Version); err != nil { if errors.Is(err, repositories.ErrVersionConflict) { return nil, apperrors.Conflict("error.version_conflict") } @@ -470,7 +470,7 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses } // Reload - task, err = s.taskRepo.FindByID(taskID) + task, err = s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { return nil, apperrors.Internal(err) } @@ -483,8 +483,8 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses // UncancelTask uncancels a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. -func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { - task, err := s.taskRepo.FindByID(taskID) +func (s *TaskService) UncancelTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -493,7 +493,7 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -501,7 +501,7 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons return nil, apperrors.Forbidden("error.task_access_denied") } - if err := s.taskRepo.Uncancel(taskID, task.Version); err != nil { + if err := s.taskRepo.WithContext(ctx).Uncancel(taskID, task.Version); err != nil { if errors.Is(err, repositories.ErrVersionConflict) { return nil, apperrors.Conflict("error.version_conflict") } @@ -509,7 +509,7 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons } // Reload - task, err = s.taskRepo.FindByID(taskID) + task, err = s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { return nil, apperrors.Internal(err) } @@ -522,8 +522,8 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons // ArchiveTask archives a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. -func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { - task, err := s.taskRepo.FindByID(taskID) +func (s *TaskService) ArchiveTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -532,7 +532,7 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -544,7 +544,7 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response return nil, apperrors.BadRequest("error.task_already_archived") } - if err := s.taskRepo.Archive(taskID, task.Version); err != nil { + if err := s.taskRepo.WithContext(ctx).Archive(taskID, task.Version); err != nil { if errors.Is(err, repositories.ErrVersionConflict) { return nil, apperrors.Conflict("error.version_conflict") } @@ -552,7 +552,7 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response } // Reload - task, err = s.taskRepo.FindByID(taskID) + task, err = s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { return nil, apperrors.Internal(err) } @@ -565,8 +565,8 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response // UnarchiveTask unarchives a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. -func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { - task, err := s.taskRepo.FindByID(taskID) +func (s *TaskService) UnarchiveTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -575,7 +575,7 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -583,7 +583,7 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon return nil, apperrors.Forbidden("error.task_access_denied") } - if err := s.taskRepo.Unarchive(taskID, task.Version); err != nil { + if err := s.taskRepo.WithContext(ctx).Unarchive(taskID, task.Version); err != nil { if errors.Is(err, repositories.ErrVersionConflict) { return nil, apperrors.Conflict("error.version_conflict") } @@ -591,7 +591,7 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon } // Reload - task, err = s.taskRepo.FindByID(taskID) + task, err = s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { return nil, apperrors.Internal(err) } @@ -606,9 +606,9 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon // CreateCompletion creates a task completion. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. -func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint, now time.Time) (*responses.TaskCompletionWithSummaryResponse, error) { +func (s *TaskService) CreateCompletion(ctx context.Context, req *requests.CreateTaskCompletionRequest, userID uint, now time.Time) (*responses.TaskCompletionWithSummaryResponse, error) { // Get the task - task, err := s.taskRepo.FindByID(req.TaskID) + task, err := s.taskRepo.WithContext(ctx).FindByID(req.TaskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -617,7 +617,7 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -653,7 +653,7 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest // Note: Frequency is no longer preloaded for performance, so we load it separately if needed var intervalDays *int if task.FrequencyID != nil { - frequency, err := s.taskRepo.GetFrequencyByID(*task.FrequencyID) + frequency, err := s.taskRepo.WithContext(ctx).GetFrequencyByID(*task.FrequencyID) if err == nil && frequency != nil { if frequency.Name == "Custom" { // Custom frequency - use task's custom_interval_days @@ -681,11 +681,11 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest // P1-5 + B-07: Wrap completion creation, task update, and image creation // in a single transaction for atomicity. If any operation fails, all are rolled back. - txErr := s.taskRepo.DB().Transaction(func(tx *gorm.DB) error { - if err := s.taskRepo.CreateCompletionTx(tx, completion); err != nil { + txErr := s.taskRepo.WithContext(ctx).DB().Transaction(func(tx *gorm.DB) error { + if err := s.taskRepo.WithContext(ctx).CreateCompletionTx(tx, completion); err != nil { return err } - if err := s.taskRepo.UpdateTx(tx, task); err != nil { + if err := s.taskRepo.WithContext(ctx).UpdateTx(tx, task); err != nil { return err } // B-07: Create images inside the same transaction as completion @@ -712,13 +712,13 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest } // Reload completion with user info and images - completion, err = s.taskRepo.FindCompletionByID(completion.ID) + completion, err = s.taskRepo.WithContext(ctx).FindCompletionByID(completion.ID) if err != nil { return nil, apperrors.Internal(err) } // Reload task with updated completions (so client can update kanban column) - task, err = s.taskRepo.FindByID(req.TaskID) + task, err = s.taskRepo.WithContext(ctx).FindByID(req.TaskID) if err != nil { // Non-fatal - still return the completion, just without the task log.Warn().Err(err).Uint("task_id", req.TaskID).Msg("Failed to reload task after completion") @@ -730,7 +730,7 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest } // Send notification to residence owner and other users - s.sendTaskCompletedNotification(task, completion) + s.sendTaskCompletedNotification(ctx, task, completion) // Return completion with updated task (includes kanban_column for UI update) resp := responses.NewTaskCompletionWithTaskResponseWithTime(completion, task, 30, now) @@ -744,9 +744,9 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest // LE-01: The entire operation (completion creation + task update) is wrapped in a // transaction for atomicity. // Returns only success/error, no response body. -func (s *TaskService) QuickComplete(taskID uint, userID uint) error { +func (s *TaskService) QuickComplete(ctx context.Context, taskID uint, userID uint) error { // Get the task - task, err := s.taskRepo.FindByID(taskID) + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return apperrors.NotFound("error.task_not_found") @@ -755,7 +755,7 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error { } // Check access - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return apperrors.Internal(err) } @@ -782,7 +782,7 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error { var quickIntervalDays *int var frequencyName = "unknown" if task.FrequencyID != nil { - frequency, err := s.taskRepo.GetFrequencyByID(*task.FrequencyID) + frequency, err := s.taskRepo.WithContext(ctx).GetFrequencyByID(*task.FrequencyID) if err == nil && frequency != nil { frequencyName = frequency.Name if frequency.Name == "Custom" { @@ -818,11 +818,11 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error { } // LE-01: Wrap completion creation and task update in a transaction for atomicity - txErr := s.taskRepo.DB().Transaction(func(tx *gorm.DB) error { - if err := s.taskRepo.CreateCompletionTx(tx, completion); err != nil { + txErr := s.taskRepo.WithContext(ctx).DB().Transaction(func(tx *gorm.DB) error { + if err := s.taskRepo.WithContext(ctx).CreateCompletionTx(tx, completion); err != nil { return err } - if err := s.taskRepo.UpdateTx(tx, task); err != nil { + if err := s.taskRepo.WithContext(ctx).UpdateTx(tx, task); err != nil { return err } return nil @@ -843,23 +843,23 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error { log.Error().Interface("panic", r).Uint("task_id", task.ID).Msg("Panic in quick-complete notification goroutine") } }() - s.sendTaskCompletedNotification(task, completion) + s.sendTaskCompletedNotification(ctx, task, completion) }() return nil } // sendTaskCompletedNotification sends notifications when a task is completed -func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) { +func (s *TaskService) sendTaskCompletedNotification(ctx context.Context, task *models.Task, completion *models.TaskCompletion) { // Get all users with access to this residence - users, err := s.residenceRepo.GetResidenceUsers(task.ResidenceID) + users, err := s.residenceRepo.WithContext(ctx).GetResidenceUsers(task.ResidenceID) if err != nil { log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to get residence users for notification") return } // Get residence name - residence, err := s.residenceRepo.FindByIDSimple(task.ResidenceID) + residence, err := s.residenceRepo.WithContext(ctx).FindByIDSimple(task.ResidenceID) residenceName := "your property" if err == nil && residence != nil { residenceName = residence.Name @@ -1000,8 +1000,8 @@ func (s *TaskService) getContentTypeFromPath(path string) string { } // GetCompletion gets a task completion by ID -func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskCompletionResponse, error) { - completion, err := s.taskRepo.FindCompletionByID(completionID) +func (s *TaskService) GetCompletion(ctx context.Context, completionID, userID uint) (*responses.TaskCompletionResponse, error) { + completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.completion_not_found") @@ -1010,7 +1010,7 @@ func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskC } // Check access via task's residence - hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(completion.Task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -1023,9 +1023,9 @@ func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskC } // ListCompletions lists all task completions for a user -func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionResponse, error) { +func (s *TaskService) ListCompletions(ctx context.Context, userID uint) ([]responses.TaskCompletionResponse, error) { // Get all residence IDs (lightweight - no preloads) - residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID) + residenceIDs, err := s.residenceRepo.WithContext(ctx).FindResidenceIDsByUser(userID) if err != nil { return nil, apperrors.Internal(err) } @@ -1034,7 +1034,7 @@ func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionRe return []responses.TaskCompletionResponse{}, nil } - completions, err := s.taskRepo.FindCompletionsByUser(userID, residenceIDs) + completions, err := s.taskRepo.WithContext(ctx).FindCompletionsByUser(userID, residenceIDs) if err != nil { return nil, apperrors.Internal(err) } @@ -1043,8 +1043,8 @@ func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionRe } // UpdateCompletion updates an existing task completion -func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.UpdateTaskCompletionRequest) (*responses.TaskCompletionResponse, error) { - completion, err := s.taskRepo.FindCompletionByID(completionID) +func (s *TaskService) UpdateCompletion(ctx context.Context, completionID, userID uint, req *requests.UpdateTaskCompletionRequest) (*responses.TaskCompletionResponse, error) { + completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.completion_not_found") @@ -1053,7 +1053,7 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests. } // Check access via task's residence - hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(completion.Task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -1072,7 +1072,7 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests. completion.Rating = req.Rating } - if err := s.taskRepo.UpdateCompletion(completion); err != nil { + if err := s.taskRepo.WithContext(ctx).UpdateCompletion(completion); err != nil { return nil, apperrors.Internal(err) } @@ -1082,13 +1082,13 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests. CompletionID: completion.ID, ImageURL: imageURL, } - if err := s.taskRepo.CreateCompletionImage(image); err != nil { + if err := s.taskRepo.WithContext(ctx).CreateCompletionImage(image); err != nil { log.Error().Err(err).Uint("completion_id", completion.ID).Msg("Failed to create completion image during update") } } // Reload to get full associations - updated, err := s.taskRepo.FindCompletionByID(completionID) + updated, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID) if err != nil { return nil, apperrors.Internal(err) } @@ -1102,8 +1102,8 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests. // P1-7: After deleting a completion, NextDueDate must be recalculated: // - If no completions remain: restore NextDueDate = DueDate (original schedule) // - If completions remain (recurring): recalculate from latest remaining completion + frequency days -func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) { - completion, err := s.taskRepo.FindCompletionByID(completionID) +func (s *TaskService) DeleteCompletion(ctx context.Context, completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) { + completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.completion_not_found") @@ -1112,7 +1112,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De } // Check access - hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(completion.Task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -1122,12 +1122,12 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De taskID := completion.TaskID - if err := s.taskRepo.DeleteCompletion(completionID); err != nil { + if err := s.taskRepo.WithContext(ctx).DeleteCompletion(completionID); err != nil { return nil, apperrors.Internal(err) } // Recalculate NextDueDate based on remaining completions - task, err := s.taskRepo.FindByID(taskID) + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { // Non-fatal for the delete operation itself, but log the error log.Error().Err(err).Uint("task_id", taskID).Msg("Failed to reload task after completion deletion for NextDueDate recalculation") @@ -1138,7 +1138,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De } // Get remaining completions for this task - remainingCompletions, err := s.taskRepo.FindCompletionsByTask(taskID) + remainingCompletions, err := s.taskRepo.WithContext(ctx).FindCompletionsByTask(taskID) if err != nil { log.Error().Err(err).Uint("task_id", taskID).Msg("Failed to query remaining completions after deletion") return &responses.DeleteWithSummaryResponse{ @@ -1150,7 +1150,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De // Determine the task's frequency interval var intervalDays *int if task.FrequencyID != nil { - frequency, freqErr := s.taskRepo.GetFrequencyByID(*task.FrequencyID) + frequency, freqErr := s.taskRepo.WithContext(ctx).GetFrequencyByID(*task.FrequencyID) if freqErr == nil && frequency != nil { if frequency.Name == "Custom" { intervalDays = task.CustomIntervalDays @@ -1175,7 +1175,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De task.NextDueDate = nil } - if err := s.taskRepo.Update(task); err != nil { + if err := s.taskRepo.WithContext(ctx).Update(task); err != nil { log.Error().Err(err).Uint("task_id", taskID).Msg("Failed to update task NextDueDate after completion deletion") // The completion was already deleted; return success but log the update failure } @@ -1187,9 +1187,9 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De } // GetCompletionsByTask gets all completions for a specific task -func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.TaskCompletionResponse, error) { +func (s *TaskService) GetCompletionsByTask(ctx context.Context, taskID, userID uint) ([]responses.TaskCompletionResponse, error) { // Get the task to check access - task, err := s.taskRepo.FindByID(taskID) + task, err := s.taskRepo.WithContext(ctx).FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, apperrors.NotFound("error.task_not_found") @@ -1198,7 +1198,7 @@ func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.Tas } // Check access via residence - hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) + hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID) if err != nil { return nil, apperrors.Internal(err) } @@ -1207,7 +1207,7 @@ func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.Tas } // Get completions for the task - completions, err := s.taskRepo.FindCompletionsByTask(taskID) + completions, err := s.taskRepo.WithContext(ctx).FindCompletionsByTask(taskID) if err != nil { return nil, apperrors.Internal(err) } @@ -1218,8 +1218,8 @@ func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.Tas // === Lookups === // GetCategories returns all task categories -func (s *TaskService) GetCategories() ([]responses.TaskCategoryResponse, error) { - categories, err := s.taskRepo.GetAllCategories() +func (s *TaskService) GetCategories(ctx context.Context) ([]responses.TaskCategoryResponse, error) { + categories, err := s.taskRepo.WithContext(ctx).GetAllCategories() if err != nil { return nil, apperrors.Internal(err) } @@ -1232,8 +1232,8 @@ func (s *TaskService) GetCategories() ([]responses.TaskCategoryResponse, error) } // GetPriorities returns all task priorities -func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error) { - priorities, err := s.taskRepo.GetAllPriorities() +func (s *TaskService) GetPriorities(ctx context.Context) ([]responses.TaskPriorityResponse, error) { + priorities, err := s.taskRepo.WithContext(ctx).GetAllPriorities() if err != nil { return nil, apperrors.Internal(err) } @@ -1246,8 +1246,8 @@ func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error) } // GetFrequencies returns all task frequencies -func (s *TaskService) GetFrequencies() ([]responses.TaskFrequencyResponse, error) { - frequencies, err := s.taskRepo.GetAllFrequencies() +func (s *TaskService) GetFrequencies(ctx context.Context) ([]responses.TaskFrequencyResponse, error) { + frequencies, err := s.taskRepo.WithContext(ctx).GetAllFrequencies() if err != nil { return nil, apperrors.Internal(err) } diff --git a/internal/services/task_service_state_transition_test.go b/internal/services/task_service_state_transition_test.go index 85c9a13..5c0e7b0 100644 --- a/internal/services/task_service_state_transition_test.go +++ b/internal/services/task_service_state_transition_test.go @@ -1,6 +1,7 @@ package services import ( + "context" "testing" "time" @@ -52,7 +53,7 @@ func TestTaskService_CompleteToCancel_OneTimeTask(t *testing.T) { TaskID: task.ID, Notes: "Completed", } - _, err = service.CreateCompletion(completionReq, user.ID, now) + _, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now) require.NoError(t, err) // Verify task is completed (NextDueDate=nil) @@ -66,7 +67,7 @@ func TestTaskService_CompleteToCancel_OneTimeTask(t *testing.T) { assert.Equal(t, "completed_tasks", column, "Completed one-time task should be in completed column") // Step 2: Cancel the completed task - cancelResp, err := service.CancelTask(task.ID, user.ID, now) + cancelResp, err := service.CancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, cancelResp.Data.IsCancelled, "Task should be cancelled") @@ -106,7 +107,7 @@ func TestTaskService_CancelToComplete(t *testing.T) { now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) // Step 1: Cancel the task - cancelResp, err := service.CancelTask(task.ID, user.ID, now) + cancelResp, err := service.CancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, cancelResp.Data.IsCancelled) @@ -115,7 +116,7 @@ func TestTaskService_CancelToComplete(t *testing.T) { TaskID: task.ID, Notes: "Completed even though cancelled", } - completionResp, err := service.CreateCompletion(completionReq, user.ID, now) + completionResp, err := service.CreateCompletion(context.Background(), completionReq, user.ID, now) require.NoError(t, err) assert.NotZero(t, completionResp.Data.ID, "Completion should be created") @@ -160,7 +161,7 @@ func TestTaskService_ArchiveToComplete(t *testing.T) { now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) // Step 1: Archive the task - archiveResp, err := service.ArchiveTask(task.ID, user.ID, now) + archiveResp, err := service.ArchiveTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, archiveResp.Data.IsArchived) @@ -169,7 +170,7 @@ func TestTaskService_ArchiveToComplete(t *testing.T) { TaskID: task.ID, Notes: "Completed even though archived", } - completionResp, err := service.CreateCompletion(completionReq, user.ID, now) + completionResp, err := service.CreateCompletion(context.Background(), completionReq, user.ID, now) require.NoError(t, err) assert.NotZero(t, completionResp.Data.ID, "Completion should be created") @@ -217,11 +218,11 @@ func TestTaskService_CompleteToArchive_OneTimeTask(t *testing.T) { TaskID: task.ID, Notes: "Done", } - _, err = service.CreateCompletion(completionReq, user.ID, now) + _, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now) require.NoError(t, err) // Step 2: Archive the completed task - archiveResp, err := service.ArchiveTask(task.ID, user.ID, now) + archiveResp, err := service.ArchiveTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, archiveResp.Data.IsArchived, "Task should be archived") @@ -266,7 +267,7 @@ func TestTaskService_InProgressToCancelToUncancel(t *testing.T) { now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) // Step 1: Mark in progress - inProgressResp, err := service.MarkInProgress(task.ID, user.ID, now) + inProgressResp, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, inProgressResp.Data.InProgress) @@ -277,7 +278,7 @@ func TestTaskService_InProgressToCancelToUncancel(t *testing.T) { assert.Equal(t, "in_progress_tasks", column, "Task should be in in_progress column") // Step 2: Cancel the in-progress task - cancelResp, err := service.CancelTask(task.ID, user.ID, now) + cancelResp, err := service.CancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, cancelResp.Data.IsCancelled) @@ -292,7 +293,7 @@ func TestTaskService_InProgressToCancelToUncancel(t *testing.T) { assert.Equal(t, "cancelled_tasks", column, "Cancelled task should be in cancelled column") // Step 3: Uncancel the task - uncancelResp, err := service.UncancelTask(task.ID, user.ID, now) + uncancelResp, err := service.UncancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.False(t, uncancelResp.Data.IsCancelled) @@ -336,15 +337,15 @@ func TestTaskService_MultipleCancelUncancelCycles(t *testing.T) { now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) // Cycle 1: Cancel -> Uncancel - _, err = service.CancelTask(task.ID, user.ID, now) + _, err = service.CancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) - _, err = service.UncancelTask(task.ID, user.ID, now) + _, err = service.UncancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) // Cycle 2: Cancel -> Uncancel - _, err = service.CancelTask(task.ID, user.ID, now) + _, err = service.CancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) - uncancelResp, err := service.UncancelTask(task.ID, user.ID, now) + uncancelResp, err := service.UncancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) // Verify final state @@ -393,7 +394,7 @@ func TestTaskService_CompleteToMarkInProgress_OneTimeTask(t *testing.T) { TaskID: task.ID, Notes: "Done", } - _, err = service.CreateCompletion(completionReq, user.ID, now) + _, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now) require.NoError(t, err) // Verify completed state @@ -402,7 +403,7 @@ func TestTaskService_CompleteToMarkInProgress_OneTimeTask(t *testing.T) { assert.Nil(t, taskAfterComplete.NextDueDate) // Step 2: Mark in progress - ipResp, err := service.MarkInProgress(task.ID, user.ID, now) + ipResp, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, ipResp.Data.InProgress, "InProgress should be true") @@ -452,7 +453,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) { now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) // Step 1: Mark in progress - ipResp, err := service.MarkInProgress(task.ID, user.ID, now) + ipResp, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, ipResp.Data.InProgress) @@ -469,7 +470,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) { Notes: "Week 1 done", CompletedAt: &firstCompletedAt, } - _, err = service.CreateCompletion(completionReq1, user.ID, now) + _, err = service.CreateCompletion(context.Background(), completionReq1, user.ID, now) require.NoError(t, err) // Verify: InProgress reset to false, NextDueDate recalculated @@ -487,7 +488,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) { assert.NotEqual(t, "completed_tasks", column, "Recurring task should not be in completed column") // Step 3: Mark in progress again for next cycle - ipResp2, err := service.MarkInProgress(task.ID, user.ID, now) + ipResp2, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, ipResp2.Data.InProgress) @@ -498,7 +499,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) { Notes: "Week 2 done", CompletedAt: &secondCompletedAt, } - _, err = service.CreateCompletion(completionReq2, user.ID, now) + _, err = service.CreateCompletion(context.Background(), completionReq2, user.ID, now) require.NoError(t, err) // Verify: InProgress reset again, NextDueDate recalculated from second completion @@ -546,7 +547,7 @@ func TestTaskService_ArchiveToUnarchive(t *testing.T) { assert.Equal(t, "due_soon_tasks", column, "Task due in 5 days should be due_soon") // Step 1: Archive - archiveResp, err := service.ArchiveTask(task.ID, user.ID, now) + archiveResp, err := service.ArchiveTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, archiveResp.Data.IsArchived) @@ -556,7 +557,7 @@ func TestTaskService_ArchiveToUnarchive(t *testing.T) { assert.Equal(t, "cancelled_tasks", column, "Archived task should be in cancelled column") // Step 2: Unarchive - unarchiveResp, err := service.UnarchiveTask(task.ID, user.ID, now) + unarchiveResp, err := service.UnarchiveTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.False(t, unarchiveResp.Data.IsArchived) @@ -738,7 +739,7 @@ func TestTaskService_OptimisticLocking_UpdateWithCorrectVersion(t *testing.T) { Title: &newTitle, } now := time.Now().UTC() - resp, err := service.UpdateTask(task.ID, user.ID, req, now) + resp, err := service.UpdateTask(context.Background(), task.ID, user.ID, req, now) require.NoError(t, err) assert.Equal(t, "Updated Title", resp.Data.Title) @@ -946,7 +947,7 @@ func TestTaskService_OptimisticLocking_ServiceCancelConflictAndRecovery(t *testi // Reset version and verify service cancel succeeds with correct version db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 1) - cancelResp, err := service.CancelTask(task.ID, user.ID, now) + cancelResp, err := service.CancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, cancelResp.Data.IsCancelled, "Service cancel should succeed with correct version") } diff --git a/internal/services/task_service_test.go b/internal/services/task_service_test.go index 2b8de00..54edc34 100644 --- a/internal/services/task_service_test.go +++ b/internal/services/task_service_test.go @@ -43,7 +43,7 @@ func TestTaskService_CreateTask(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) assert.NotZero(t, resp.Data.ID) assert.Equal(t, "Fix leaky faucet", resp.Data.Title) @@ -79,7 +79,7 @@ func TestTaskService_CreateTask_WithOptionalFields(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) // Note: Category and Priority are no longer preloaded for performance // Client resolves from cache using CategoryID and PriorityID @@ -120,7 +120,7 @@ func TestTaskService_CreateTask_WithTemplateID(t *testing.T) { Title: "From template: " + tc.name, TemplateID: tc.templateID, } - resp, err := service.CreateTask(req, user.ID, time.Now().UTC()) + resp, err := service.CreateTask(context.Background(), req, user.ID, time.Now().UTC()) require.NoError(t, err) if tc.wantID == nil { @@ -165,7 +165,7 @@ func TestTaskService_BulkCreateTasks(t *testing.T) { {ResidenceID: residence.ID, Title: "Task C"}, }, } - resp, err := service.BulkCreateTasks(req, user.ID, time.Now().UTC()) + resp, err := service.BulkCreateTasks(context.Background(), req, user.ID, time.Now().UTC()) require.NoError(t, err) assert.Equal(t, 3, resp.CreatedCount) assert.Len(t, resp.Tasks, 3) @@ -198,7 +198,7 @@ func TestTaskService_BulkCreateTasks(t *testing.T) { ResidenceID: residence.ID, Tasks: []requests.CreateTaskRequest{}, // empty triggers the guard } - _, err := service.BulkCreateTasks(req, user.ID, time.Now().UTC()) + _, err := service.BulkCreateTasks(context.Background(), req, user.ID, time.Now().UTC()) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.task_list_empty") var after int64 @@ -214,7 +214,7 @@ func TestTaskService_BulkCreateTasks(t *testing.T) { {ResidenceID: residence.ID, Title: "Sneaky"}, }, } - _, err := service.BulkCreateTasks(req, other.ID, time.Now().UTC()) + _, err := service.BulkCreateTasks(context.Background(), req, other.ID, time.Now().UTC()) testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") }) @@ -227,7 +227,7 @@ func TestTaskService_BulkCreateTasks(t *testing.T) { {ResidenceID: second.ID, Title: "Should land on batch residence"}, }, } - resp, err := service.BulkCreateTasks(req, user.ID, time.Now().UTC()) + resp, err := service.BulkCreateTasks(context.Background(), req, user.ID, time.Now().UTC()) require.NoError(t, err) require.Len(t, resp.Tasks, 1) assert.Equal(t, residence.ID, resp.Tasks[0].ResidenceID) @@ -251,7 +251,7 @@ func TestTaskService_CreateTask_AccessDenied(t *testing.T) { } now := time.Now().UTC() - _, err := service.CreateTask(req, otherUser.ID, now) + _, err := service.CreateTask(context.Background(), req, otherUser.ID, now) // When creating a task, residence access is checked first testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") } @@ -267,7 +267,7 @@ func TestTaskService_GetTask(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") - resp, err := service.GetTask(task.ID, user.ID) + resp, err := service.GetTask(context.Background(), task.ID, user.ID) require.NoError(t, err) assert.Equal(t, task.ID, resp.ID) assert.Equal(t, "Test Task", resp.Title) @@ -285,7 +285,7 @@ func TestTaskService_GetTask_AccessDenied(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, owner.ID, "Test Task") - _, err := service.GetTask(task.ID, otherUser.ID) + _, err := service.GetTask(context.Background(), task.ID, otherUser.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.task_access_denied") } @@ -332,7 +332,7 @@ func TestTaskService_UpdateTask(t *testing.T) { } now := time.Now().UTC() - resp, err := service.UpdateTask(task.ID, user.ID, req, now) + resp, err := service.UpdateTask(context.Background(), task.ID, user.ID, req, now) require.NoError(t, err) assert.Equal(t, "Updated Title", resp.Data.Title) assert.Equal(t, "Updated description", resp.Data.Description) @@ -349,10 +349,10 @@ func TestTaskService_DeleteTask(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") - _, err := service.DeleteTask(task.ID, user.ID) + _, err := service.DeleteTask(context.Background(), task.ID, user.ID) require.NoError(t, err) - _, err = service.GetTask(task.ID, user.ID) + _, err = service.GetTask(context.Background(), task.ID, user.ID) assert.Error(t, err) } @@ -368,7 +368,7 @@ func TestTaskService_CancelTask(t *testing.T) { task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") now := time.Now().UTC() - resp, err := service.CancelTask(task.ID, user.ID, now) + resp, err := service.CancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, resp.Data.IsCancelled) } @@ -385,8 +385,8 @@ func TestTaskService_CancelTask_AlreadyCancelled(t *testing.T) { task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") now := time.Now().UTC() - service.CancelTask(task.ID, user.ID, now) - _, err := service.CancelTask(task.ID, user.ID, now) + service.CancelTask(context.Background(), task.ID, user.ID, now) + _, err := service.CancelTask(context.Background(), task.ID, user.ID, now) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.task_already_cancelled") } @@ -402,8 +402,8 @@ func TestTaskService_UncancelTask(t *testing.T) { task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") now := time.Now().UTC() - service.CancelTask(task.ID, user.ID, now) - resp, err := service.UncancelTask(task.ID, user.ID, now) + service.CancelTask(context.Background(), task.ID, user.ID, now) + resp, err := service.UncancelTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.False(t, resp.Data.IsCancelled) } @@ -420,7 +420,7 @@ func TestTaskService_ArchiveTask(t *testing.T) { task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") now := time.Now().UTC() - resp, err := service.ArchiveTask(task.ID, user.ID, now) + resp, err := service.ArchiveTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, resp.Data.IsArchived) } @@ -437,8 +437,8 @@ func TestTaskService_UnarchiveTask(t *testing.T) { task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") now := time.Now().UTC() - service.ArchiveTask(task.ID, user.ID, now) - resp, err := service.UnarchiveTask(task.ID, user.ID, now) + service.ArchiveTask(context.Background(), task.ID, user.ID, now) + resp, err := service.UnarchiveTask(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.False(t, resp.Data.IsArchived) } @@ -455,7 +455,7 @@ func TestTaskService_MarkInProgress(t *testing.T) { task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") now := time.Now().UTC() - resp, err := service.MarkInProgress(task.ID, user.ID, now) + resp, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now) require.NoError(t, err) assert.True(t, resp.Data.InProgress) } @@ -477,7 +477,7 @@ func TestTaskService_CreateCompletion(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateCompletion(req, user.ID, now) + resp, err := service.CreateCompletion(context.Background(), req, user.ID, now) require.NoError(t, err) assert.NotZero(t, resp.Data.ID) assert.Equal(t, task.ID, resp.Data.TaskID) @@ -520,7 +520,7 @@ func TestTaskService_CreateCompletion_RecurringTask_ResetsInProgress(t *testing. } now := time.Now().UTC() - resp, err := service.CreateCompletion(req, user.ID, now) + resp, err := service.CreateCompletion(context.Background(), req, user.ID, now) require.NoError(t, err) assert.NotZero(t, resp.Data.ID) @@ -558,7 +558,7 @@ func TestTaskService_GetCompletion(t *testing.T) { } db.Create(completion) - resp, err := service.GetCompletion(completion.ID, user.ID) + resp, err := service.GetCompletion(context.Background(), completion.ID, user.ID) require.NoError(t, err) assert.Equal(t, completion.ID, resp.ID) assert.Equal(t, "Test notes", resp.Notes) @@ -582,10 +582,10 @@ func TestTaskService_DeleteCompletion(t *testing.T) { } db.Create(completion) - _, err := service.DeleteCompletion(completion.ID, user.ID) + _, err := service.DeleteCompletion(context.Background(), completion.ID, user.ID) require.NoError(t, err) - _, err = service.GetCompletion(completion.ID, user.ID) + _, err = service.GetCompletion(context.Background(), completion.ID, user.ID) assert.Error(t, err) } @@ -623,7 +623,7 @@ func TestTaskService_CreateCompletion_TransactionIntegrity(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateCompletion(req, user.ID, now) + resp, err := service.CreateCompletion(context.Background(), req, user.ID, now) require.NoError(t, err) assert.NotZero(t, resp.Data.ID) @@ -721,7 +721,7 @@ func TestTaskService_CreateCompletion_UpdateError_ReturnedNotSwallowed(t *testin now := time.Now().UTC() // This call will succeed because FindByID loads version=1, UpdateTx uses version=1, DB has version=1. // To verify error propagation, we use the direct transaction test above. - resp, err := service.CreateCompletion(req, user.ID, now) + resp, err := service.CreateCompletion(context.Background(), req, user.ID, now) require.NoError(t, err, "CreateCompletion should succeed with matching versions") assert.NotZero(t, resp.Data.ID) } @@ -760,7 +760,7 @@ func TestTaskService_DeleteCompletion_OneTime_RestoresOriginalDueDate(t *testing Notes: "Completed", } now := time.Now().UTC() - completionResp, err := service.CreateCompletion(req, user.ID, now) + completionResp, err := service.CreateCompletion(context.Background(), req, user.ID, now) require.NoError(t, err) // Confirm NextDueDate is nil after completion @@ -769,7 +769,7 @@ func TestTaskService_DeleteCompletion_OneTime_RestoresOriginalDueDate(t *testing assert.Nil(t, taskAfterComplete.NextDueDate, "NextDueDate should be nil after one-time completion") // Delete the completion - _, err = service.DeleteCompletion(completionResp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), completionResp.Data.ID, user.ID) require.NoError(t, err) // Verify NextDueDate is restored to the original DueDate @@ -820,7 +820,7 @@ func TestTaskService_DeleteCompletion_Recurring_RecalculatesFromLastCompletion(t CompletedAt: &firstCompletedAt, } now := time.Now().UTC() - _, err = service.CreateCompletion(firstReq, user.ID, now) + _, err = service.CreateCompletion(context.Background(), firstReq, user.ID, now) require.NoError(t, err) // Second completion on Feb 15 @@ -830,7 +830,7 @@ func TestTaskService_DeleteCompletion_Recurring_RecalculatesFromLastCompletion(t Notes: "Second completion", CompletedAt: &secondCompletedAt, } - resp, err := service.CreateCompletion(secondReq, user.ID, now) + resp, err := service.CreateCompletion(context.Background(), secondReq, user.ID, now) require.NoError(t, err) // NextDueDate should be Feb 15 + 30 days = Mar 17 @@ -843,7 +843,7 @@ func TestTaskService_DeleteCompletion_Recurring_RecalculatesFromLastCompletion(t assert.Equal(t, expectedAfterSecond.Day(), taskAfterSecond.NextDueDate.Day()) // Delete the second (latest) completion - _, err = service.DeleteCompletion(resp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), resp.Data.ID, user.ID) require.NoError(t, err) // NextDueDate should be recalculated from the first completion: Jan 15 + 30 = Feb 14 @@ -895,7 +895,7 @@ func TestTaskService_DeleteCompletion_LastCompletion_RestoresDueDate(t *testing. CompletedAt: &completedAt, } now := time.Now().UTC() - completionResp, err := service.CreateCompletion(req, user.ID, now) + completionResp, err := service.CreateCompletion(context.Background(), req, user.ID, now) require.NoError(t, err) // Verify NextDueDate was set to completedAt + 7 days @@ -904,7 +904,7 @@ func TestTaskService_DeleteCompletion_LastCompletion_RestoresDueDate(t *testing. require.NotNil(t, taskAfterComplete.NextDueDate) // Delete the only completion - _, err = service.DeleteCompletion(completionResp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), completionResp.Data.ID, user.ID) require.NoError(t, err) // NextDueDate should be restored to original DueDate since no completions remain @@ -923,7 +923,7 @@ func TestTaskService_GetCategories(t *testing.T) { residenceRepo := repositories.NewResidenceRepository(db) service := NewTaskService(taskRepo, residenceRepo) - categories, err := service.GetCategories() + categories, err := service.GetCategories(context.Background()) require.NoError(t, err) assert.Greater(t, len(categories), 0) @@ -941,7 +941,7 @@ func TestTaskService_GetPriorities(t *testing.T) { residenceRepo := repositories.NewResidenceRepository(db) service := NewTaskService(taskRepo, residenceRepo) - priorities, err := service.GetPriorities() + priorities, err := service.GetPriorities(context.Background()) require.NoError(t, err) assert.Greater(t, len(priorities), 0) @@ -958,7 +958,7 @@ func TestTaskService_GetFrequencies(t *testing.T) { residenceRepo := repositories.NewResidenceRepository(db) service := NewTaskService(taskRepo, residenceRepo) - frequencies, err := service.GetFrequencies() + frequencies, err := service.GetFrequencies(context.Background()) require.NoError(t, err) assert.Greater(t, len(frequencies), 0) } @@ -981,7 +981,7 @@ func TestTaskService_SharedUserAccess(t *testing.T) { task := testutil.CreateTestTask(t, db, residence.ID, owner.ID, "Test Task") // Shared user should be able to see the task - resp, err := service.GetTask(task.ID, sharedUser.ID) + resp, err := service.GetTask(context.Background(), task.ID, sharedUser.ID) require.NoError(t, err) assert.Equal(t, task.ID, resp.ID) @@ -991,7 +991,7 @@ func TestTaskService_SharedUserAccess(t *testing.T) { Title: "Shared User Task", } now := time.Now().UTC() - _, err = service.CreateTask(req, sharedUser.ID, now) + _, err = service.CreateTask(context.Background(), req, sharedUser.ID, now) require.NoError(t, err) } @@ -1017,7 +1017,7 @@ func TestTaskService_CreateTask_NextDueDateEqualsDueDate(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) require.NotNil(t, resp.Data.DueDate, "DueDate should be set") require.NotNil(t, resp.Data.NextDueDate, "NextDueDate should be initialized") @@ -1041,7 +1041,7 @@ func TestTaskService_CreateTask_NoDueDate_NextDueDateNil(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) assert.Nil(t, resp.Data.DueDate, "DueDate should be nil") assert.Nil(t, resp.Data.NextDueDate, "NextDueDate should be nil when no DueDate") @@ -1073,7 +1073,7 @@ func TestTaskService_CreateTask_WithWeeklyFrequency(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved") assert.Equal(t, weeklyFreq.ID, *resp.Data.FrequencyID, "FrequencyID should match Weekly") @@ -1105,7 +1105,7 @@ func TestTaskService_CreateTask_WithCustomFrequencyAndInterval(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved") assert.NotNil(t, resp.Data.CustomIntervalDays, "CustomIntervalDays should be saved") @@ -1132,7 +1132,7 @@ func TestTaskService_CreateTask_FrequencyWithoutDueDate(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved") assert.Nil(t, resp.Data.NextDueDate, "NextDueDate should be nil when no DueDate") @@ -1160,7 +1160,7 @@ func TestTaskService_CreateTask_WithPastDueDate(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err, "Creating task with past due date should not error") require.NotNil(t, resp.Data.DueDate, "DueDate should be set") assert.True(t, resp.Data.DueDate.Before(time.Now()), "DueDate should be in the past") @@ -1183,7 +1183,7 @@ func TestTaskService_CreateTask_WithInProgressTrue(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) assert.True(t, resp.Data.InProgress, "InProgress should be true in response") @@ -1234,7 +1234,7 @@ func TestTaskService_CreateTask_AllOptionalFields(t *testing.T) { } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err) assert.Equal(t, "Full task with all fields", resp.Data.Title) @@ -1272,7 +1272,7 @@ func TestTaskService_CreateTask_CustomIntervalDaysWithoutFrequency(t *testing.T) } now := time.Now().UTC() - resp, err := service.CreateTask(req, user.ID, now) + resp, err := service.CreateTask(context.Background(), req, user.ID, now) require.NoError(t, err, "Should save even without FrequencyID") assert.NotNil(t, resp.Data.CustomIntervalDays, "CustomIntervalDays should be saved") assert.Equal(t, 10, *resp.Data.CustomIntervalDays) @@ -1300,7 +1300,7 @@ func TestTaskService_CreateTask_InvalidResidenceAccess(t *testing.T) { } now := time.Now().UTC() - _, err := service.CreateTask(req, stranger.ID, now) + _, err := service.CreateTask(context.Background(), req, stranger.ID, now) testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") } @@ -1319,7 +1319,7 @@ func TestTaskService_CreateTask_NonExistentResidence(t *testing.T) { } now := time.Now().UTC() - _, err := service.CreateTask(req, user.ID, now) + _, err := service.CreateTask(context.Background(), req, user.ID, now) require.Error(t, err, "Should error for non-existent residence") // Should return forbidden since user has no access to non-existent residence testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") @@ -1357,7 +1357,7 @@ func TestTaskService_DeleteCompletion_OneTime_RestoresAndExitsKanbanCompleted(t // Complete the task (sets NextDueDate to nil) now := time.Now().UTC() - completionResp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + completionResp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Done", }, user.ID, now) @@ -1373,7 +1373,7 @@ func TestTaskService_DeleteCompletion_OneTime_RestoresAndExitsKanbanCompleted(t "Task should be in completed state") // Delete the completion - _, err = service.DeleteCompletion(completionResp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), completionResp.Data.ID, user.ID) require.NoError(t, err) // Verify NextDueDate restored to original DueDate @@ -1425,7 +1425,7 @@ func TestTaskService_DeleteCompletion_Recurring_RestoresToOriginalDueDate(t *tes // Complete on Apr 3 completedAt := time.Date(2026, 4, 3, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - completionResp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + completionResp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Weekly done", CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -1437,7 +1437,7 @@ func TestTaskService_DeleteCompletion_Recurring_RestoresToOriginalDueDate(t *tes assert.Equal(t, 10, taskAfterComplete.NextDueDate.Day()) // Delete the completion - _, err = service.DeleteCompletion(completionResp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), completionResp.Data.ID, user.ID) require.NoError(t, err) // Verify NextDueDate restored to original DueDate (Apr 1) @@ -1483,21 +1483,21 @@ func TestTaskService_DeleteCompletion_MultipleCompletions_SequentialDeletion(t * // Completion 1: Jan 5 c1At := time.Date(2026, 1, 5, 10, 0, 0, 0, time.UTC) - c1Resp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + c1Resp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Completion 1", CompletedAt: &c1At, }, user.ID, now) require.NoError(t, err) // Completion 2: Jan 12 c2At := time.Date(2026, 1, 12, 10, 0, 0, 0, time.UTC) - c2Resp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + c2Resp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Completion 2", CompletedAt: &c2At, }, user.ID, now) require.NoError(t, err) // Completion 3: Jan 19 c3At := time.Date(2026, 1, 19, 10, 0, 0, 0, time.UTC) - c3Resp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + c3Resp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Completion 3", CompletedAt: &c3At, }, user.ID, now) require.NoError(t, err) @@ -1509,7 +1509,7 @@ func TestTaskService_DeleteCompletion_MultipleCompletions_SequentialDeletion(t * assert.Equal(t, 26, taskAfter3.NextDueDate.Day()) // Delete completion 3 (latest) -> recalc from completion 2: Jan 12 + 7 = Jan 19 - _, err = service.DeleteCompletion(c3Resp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), c3Resp.Data.ID, user.ID) require.NoError(t, err) var taskAfterDel3 models.Task db.First(&taskAfterDel3, task.ID) @@ -1518,7 +1518,7 @@ func TestTaskService_DeleteCompletion_MultipleCompletions_SequentialDeletion(t * "NextDueDate should be Jan 19 (completion 2: Jan 12 + 7)") // Delete completion 2 (now latest) -> recalc from completion 1: Jan 5 + 7 = Jan 12 - _, err = service.DeleteCompletion(c2Resp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), c2Resp.Data.ID, user.ID) require.NoError(t, err) var taskAfterDel2 models.Task db.First(&taskAfterDel2, task.ID) @@ -1527,7 +1527,7 @@ func TestTaskService_DeleteCompletion_MultipleCompletions_SequentialDeletion(t * "NextDueDate should be Jan 12 (completion 1: Jan 5 + 7)") // Delete completion 1 (last remaining) -> restore to original DueDate: Jan 1 - _, err = service.DeleteCompletion(c1Resp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), c1Resp.Data.ID, user.ID) require.NoError(t, err) var taskAfterDel1 models.Task db.First(&taskAfterDel1, task.ID) @@ -1570,19 +1570,19 @@ func TestTaskService_DeleteCompletion_MiddleCompletion_KeepsLatest(t *testing.T) // Create 3 completions c1At := time.Date(2026, 2, 3, 10, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "First", CompletedAt: &c1At, }, user.ID, now) require.NoError(t, err) c2At := time.Date(2026, 2, 10, 10, 0, 0, 0, time.UTC) - c2Resp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + c2Resp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Second", CompletedAt: &c2At, }, user.ID, now) require.NoError(t, err) c3At := time.Date(2026, 2, 17, 10, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Third", CompletedAt: &c3At, }, user.ID, now) require.NoError(t, err) @@ -1594,7 +1594,7 @@ func TestTaskService_DeleteCompletion_MiddleCompletion_KeepsLatest(t *testing.T) assert.Equal(t, 24, taskBefore.NextDueDate.Day()) // Delete the MIDDLE completion (2nd) - _, err = service.DeleteCompletion(c2Resp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), c2Resp.Data.ID, user.ID) require.NoError(t, err) // NextDueDate should still be based on the latest (3rd): Feb 17 + 7 = Feb 24 @@ -1634,7 +1634,7 @@ func TestTaskService_DeleteCompletion_DoesNotRestoreInProgress(t *testing.T) { // Complete it (sets InProgress = false) now := time.Now().UTC() - completionResp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + completionResp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Completed", }, user.ID, now) require.NoError(t, err) @@ -1644,7 +1644,7 @@ func TestTaskService_DeleteCompletion_DoesNotRestoreInProgress(t *testing.T) { assert.False(t, taskAfterComplete.InProgress, "InProgress should be false after completion") // Delete the completion - _, err = service.DeleteCompletion(completionResp.Data.ID, user.ID) + _, err = service.DeleteCompletion(context.Background(), completionResp.Data.ID, user.ID) require.NoError(t, err) // InProgress is NOT restored by DeleteCompletion @@ -1674,7 +1674,7 @@ func TestTaskService_TaskWithNoDates(t *testing.T) { Title: "No Date Task", } now := time.Now().UTC() - createResp, err := service.CreateTask(createReq, user.ID, now) + createResp, err := service.CreateTask(context.Background(), createReq, user.ID, now) require.NoError(t, err) // Verify dates are nil @@ -1686,7 +1686,7 @@ func TestTaskService_TaskWithNoDates(t *testing.T) { "Task with no due date should be in upcoming column") // Complete it - completionResp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + completionResp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: createResp.Data.ID, Notes: "Done", }, user.ID, now) require.NoError(t, err) @@ -1732,7 +1732,7 @@ func TestTaskService_TaskDueExactlyToday_Boundary(t *testing.T) { assert.False(t, task.IsOverdueAt(today), "Task due today should NOT be overdue") // Complete it - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Done today", }, user.ID, today) require.NoError(t, err) @@ -1771,7 +1771,7 @@ func TestTaskService_TaskDueYesterday_IsOverdue(t *testing.T) { assert.True(t, task.IsOverdueAt(today), "Task due yesterday should be overdue") // Complete it - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Done late", }, user.ID, today) require.NoError(t, err) @@ -1802,7 +1802,7 @@ func TestTaskService_TaskVeryFarFuture(t *testing.T) { DueDate: &requests.FlexibleDate{Time: farFuture}, } now := time.Now().UTC() - resp, err := service.CreateTask(createReq, user.ID, now) + resp, err := service.CreateTask(context.Background(), createReq, user.ID, now) require.NoError(t, err) assert.Equal(t, "upcoming_tasks", resp.Data.KanbanColumn, @@ -1840,7 +1840,7 @@ func TestTaskService_RecurringTask_CompletedLate(t *testing.T) { completedAt := time.Date(2026, 12, 15, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Late", CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -1884,7 +1884,7 @@ func TestTaskService_RecurringTask_CompletedEarly(t *testing.T) { completedAt := time.Date(2026, 12, 14, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Early", CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -1937,7 +1937,7 @@ func TestTaskService_CustomIntervalEdgeCases(t *testing.T) { completedAt := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -1968,7 +1968,7 @@ func TestTaskService_CustomIntervalEdgeCases(t *testing.T) { completedAt := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2014,7 +2014,7 @@ func TestTaskService_FrequencyWithNilOrZeroDays(t *testing.T) { require.NoError(t, err) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Done once", }, user.ID, now) require.NoError(t, err) @@ -2049,7 +2049,7 @@ func TestTaskService_FrequencyWithNilOrZeroDays(t *testing.T) { require.NoError(t, err) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Done zero", }, user.ID, now) require.NoError(t, err) @@ -2076,7 +2076,7 @@ func TestTaskService_VersionConflict(t *testing.T) { // First update succeeds newTitle := "Updated Once" now := time.Now().UTC() - resp, err := service.UpdateTask(task.ID, user.ID, &requests.UpdateTaskRequest{ + resp, err := service.UpdateTask(context.Background(), task.ID, user.ID, &requests.UpdateTaskRequest{ Title: &newTitle, }, now) require.NoError(t, err) @@ -2154,7 +2154,7 @@ func TestTaskService_CreateCompletion_OneTime_NextDueDateBecomesNil(t *testing.T require.NoError(t, err) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Done", }, user.ID, now) require.NoError(t, err) @@ -2190,7 +2190,7 @@ func TestTaskService_CreateCompletion_OneTime_InProgressBecomesFalse(t *testing. require.NoError(t, err) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, }, user.ID, now) require.NoError(t, err) @@ -2212,7 +2212,7 @@ func TestTaskService_CreateCompletion_OneTime_CompletionRecordFields(t *testing. task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Record Fields Task") now := time.Now().UTC() - resp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + resp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Check all fields", }, user.ID, now) require.NoError(t, err) @@ -2241,7 +2241,7 @@ func TestTaskService_CreateCompletion_OneTime_WithNotesActualCostRating(t *testi cost := decimal.NewFromFloat(75.50) rating := 4 now := time.Now().UTC() - resp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + resp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Detailed notes", ActualCost: &cost, @@ -2292,7 +2292,7 @@ func TestTaskService_CreateCompletion_AlreadyCompleted_SecondCompletionCreated(t now := time.Now().UTC() // First completion - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "First", }, user.ID, now) require.NoError(t, err) @@ -2302,7 +2302,7 @@ func TestTaskService_CreateCompletion_AlreadyCompleted_SecondCompletionCreated(t assert.Nil(t, afterFirst.NextDueDate, "NextDueDate should be nil after first completion") // Second completion on already-completed task - resp2, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + resp2, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Second", }, user.ID, now) require.NoError(t, err) @@ -2332,7 +2332,7 @@ func TestTaskService_CreateCompletion_WithBackdatedCompletedAt(t *testing.T) { backdated := time.Date(2026, 1, 15, 14, 30, 0, 0, time.UTC) now := time.Now().UTC() - _, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Backdated", CompletedAt: &backdated, }, user.ID, now) require.NoError(t, err) @@ -2375,7 +2375,7 @@ func TestTaskService_CreateCompletion_Recurring_BackdatedCompletedAt_NextDueFrom backdated := time.Date(2026, 1, 10, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, Notes: "Backdated recurring", CompletedAt: &backdated, }, user.ID, now) require.NoError(t, err) @@ -2425,7 +2425,7 @@ func TestTaskService_CreateCompletion_Recurring_Daily(t *testing.T) { now := time.Now().UTC() completedAt1 := time.Date(2026, 3, 26, 9, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt1, }, user.ID, now) require.NoError(t, err) @@ -2441,7 +2441,7 @@ func TestTaskService_CreateCompletion_Recurring_Daily(t *testing.T) { // Complete again completedAt2 := time.Date(2026, 3, 27, 10, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt2, }, user.ID, now) require.NoError(t, err) @@ -2483,7 +2483,7 @@ func TestTaskService_CreateCompletion_Recurring_Weekly_OnTime(t *testing.T) { completedAt := time.Date(2026, 3, 20, 15, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2527,7 +2527,7 @@ func TestTaskService_CreateCompletion_Recurring_Weekly_CompletedLate_NextDueFrom // Complete 3 days late completedAt := time.Date(2026, 3, 23, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2572,7 +2572,7 @@ func TestTaskService_CreateCompletion_Recurring_BiWeekly(t *testing.T) { completedAt := time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2616,7 +2616,7 @@ func TestTaskService_CreateCompletion_Recurring_Monthly_Dedicated(t *testing.T) completedAt := time.Date(2026, 2, 5, 12, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2662,7 +2662,7 @@ func TestTaskService_CreateCompletion_Recurring_Quarterly(t *testing.T) { completedAt := time.Date(2026, 1, 5, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2708,7 +2708,7 @@ func TestTaskService_CreateCompletion_Recurring_Yearly(t *testing.T) { completedAt := time.Date(2026, 1, 10, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2766,7 +2766,7 @@ func TestTaskService_CreateCompletion_Recurring_Custom_Intervals(t *testing.T) { completedAt := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2812,7 +2812,7 @@ func TestTaskService_CreateCompletion_Recurring_NoDueDate_NextDueFromCompletion( completedAt := time.Date(2026, 3, 26, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -2859,7 +2859,7 @@ func TestTaskService_CreateCompletion_Recurring_ThreeSequentialCompletions(t *te // Completion 1: Mar 1 -> NextDueDate = Mar 8 c1 := time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &c1, }, user.ID, now) require.NoError(t, err) @@ -2869,7 +2869,7 @@ func TestTaskService_CreateCompletion_Recurring_ThreeSequentialCompletions(t *te // Completion 2: Mar 8 -> NextDueDate = Mar 15 c2 := time.Date(2026, 3, 8, 10, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &c2, }, user.ID, now) require.NoError(t, err) @@ -2879,7 +2879,7 @@ func TestTaskService_CreateCompletion_Recurring_ThreeSequentialCompletions(t *te // Completion 3: Mar 15 -> NextDueDate = Mar 22 c3 := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &c3, }, user.ID, now) require.NoError(t, err) @@ -2922,7 +2922,7 @@ func TestTaskService_CreateCompletion_Recurring_InProgressResetForNextCycle(t *t require.NoError(t, err) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, }, user.ID, now) require.NoError(t, err) @@ -2960,7 +2960,7 @@ func TestTaskService_CreateCompletion_CompletedFromColumn_Capture(t *testing.T) require.NoError(t, err) now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, }, user.ID, now) require.NoError(t, err) @@ -2988,7 +2988,7 @@ func TestTaskService_CreateCompletion_CompletedFromColumn_Capture(t *testing.T) require.NoError(t, err) now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, }, user.ID, now) require.NoError(t, err) @@ -3047,7 +3047,7 @@ func TestTaskService_CreateCompletion_AllFrequencyTypes_TableDriven(t *testing.T completedAt := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC) now := time.Now().UTC() - _, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ + _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{ TaskID: task.ID, CompletedAt: &completedAt, }, user.ID, now) require.NoError(t, err) @@ -3091,7 +3091,7 @@ func TestTaskService_QuickComplete_OneTime_ClearsNextDueDate(t *testing.T) { err := db.Create(task).Error require.NoError(t, err) - err = service.QuickComplete(task.ID, user.ID) + err = service.QuickComplete(context.Background(), task.ID, user.ID) require.NoError(t, err) var reloaded models.Task @@ -3132,7 +3132,7 @@ func TestTaskService_QuickComplete_Recurring_RecalculatesNextDueDate(t *testing. err := db.Create(task).Error require.NoError(t, err) - err = service.QuickComplete(task.ID, user.ID) + err = service.QuickComplete(context.Background(), task.ID, user.ID) require.NoError(t, err) var reloaded models.Task @@ -3154,7 +3154,7 @@ func TestTaskService_QuickComplete_SetsWidgetNotes(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Widget Task") - err := service.QuickComplete(task.ID, user.ID) + err := service.QuickComplete(context.Background(), task.ID, user.ID) require.NoError(t, err) var completion models.TaskCompletion @@ -3173,7 +3173,7 @@ func TestTaskService_QuickComplete_NonExistentTask_ReturnsNotFound(t *testing.T) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") - err := service.QuickComplete(99999, user.ID) + err := service.QuickComplete(context.Background(), 99999, user.ID) testutil.AssertAppErrorCode(t, err, http.StatusNotFound) } @@ -3189,6 +3189,6 @@ func TestTaskService_QuickComplete_AccessDenied_ReturnsForbidden(t *testing.T) { residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, owner.ID, "Private Task") - err := service.QuickComplete(task.ID, otherUser.ID) + err := service.QuickComplete(context.Background(), task.ID, otherUser.ID) testutil.AssertAppError(t, err, http.StatusForbidden, "error.task_access_denied") }