Migrate TaskService + ResidenceService to ctx-aware repos
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled

Every public method on TaskService and ResidenceService now takes
ctx context.Context as the first arg and routes its repo calls through
.WithContext(ctx). With otelgorm registered, this means every API
endpoint backed by these two services produces a flame graph in Jaeger
where the SQL spans nest under the parent HTTP request span — instead
of appearing as orphaned queries.

Endpoints now fully traced (HTTP → service → SQL):
- GET    /api/tasks/                       (already shipped)
- GET    /api/tasks/by-residence/:id/      (already shipped)
- GET    /api/tasks/:id/
- POST   /api/tasks/
- POST   /api/tasks/bulk/
- PUT    /api/tasks/:id/
- DELETE /api/tasks/:id/
- POST   /api/tasks/:id/in-progress/
- POST   /api/tasks/:id/cancel/
- POST   /api/tasks/:id/uncancel/
- POST   /api/tasks/:id/archive/
- POST   /api/tasks/:id/unarchive/
- POST   /api/tasks/:id/complete/
- POST   /api/tasks/:id/quick-complete/
- GET    /api/tasks/completions/* (CRUD)
- GET    /api/static_data/ (categories, priorities, frequencies)
- GET    /api/residences/
- GET    /api/residences/my/
- GET    /api/residences/summary/
- GET    /api/residences/:id/
- POST   /api/residences/
- PUT    /api/residences/:id/
- DELETE /api/residences/:id/
- Share-code + member management endpoints
- GET    /api/residences/:id/report/

Mechanical work: ~50 method signatures, ~80 handler call sites,
~25 test call sites updated. Internal sendTaskCompletedNotification
helper also takes ctx so background notification SQL nests correctly.

The remaining services (ContractorService, DocumentService,
AuthService, NotificationService, SubscriptionService) follow the same
pattern; they continue to emit untraced SQL until migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-25 16:04:01 -05:00
parent 3f5bf21e09
commit 65a9aae4e5
9 changed files with 382 additions and 378 deletions
+15 -15
View File
@@ -39,7 +39,7 @@ func (h *ResidenceHandler) ListResidences(c echo.Context) error {
return err return err
} }
response, err := h.residenceService.ListResidences(user.ID) response, err := h.residenceService.ListResidences(c.Request().Context(), user.ID)
if err != nil { if err != nil {
return err return err
} }
@@ -55,7 +55,7 @@ func (h *ResidenceHandler) GetMyResidences(c echo.Context) error {
} }
userNow := middleware.GetUserNow(c) 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 { if err != nil {
return err return err
} }
@@ -72,7 +72,7 @@ func (h *ResidenceHandler) GetSummary(c echo.Context) error {
} }
userNow := middleware.GetUserNow(c) 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 { if err != nil {
return err return err
} }
@@ -93,7 +93,7 @@ func (h *ResidenceHandler) GetResidence(c echo.Context) error {
} }
userNow := middleware.GetUserNow(c) 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 { if err != nil {
return err return err
} }
@@ -116,7 +116,7 @@ func (h *ResidenceHandler) CreateResidence(c echo.Context) error {
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) 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 { if err != nil {
return err return err
} }
@@ -144,7 +144,7 @@ func (h *ResidenceHandler) UpdateResidence(c echo.Context) error {
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err)) 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 { if err != nil {
return err return err
} }
@@ -164,7 +164,7 @@ func (h *ResidenceHandler) DeleteResidence(c echo.Context) error {
return apperrors.BadRequest("error.invalid_residence_id") 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 { if err != nil {
return err return err
} }
@@ -185,7 +185,7 @@ func (h *ResidenceHandler) GetShareCode(c echo.Context) error {
return apperrors.BadRequest("error.invalid_residence_id") 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 { if err != nil {
return err return err
} }
@@ -213,7 +213,7 @@ func (h *ResidenceHandler) GenerateShareCode(c echo.Context) error {
// Request body is optional // Request body is optional
c.Bind(&req) 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 { if err != nil {
return err return err
} }
@@ -238,7 +238,7 @@ func (h *ResidenceHandler) GenerateSharePackage(c echo.Context) error {
// Request body is optional (for expires_in_hours) // Request body is optional (for expires_in_hours)
c.Bind(&req) 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 { if err != nil {
return err return err
} }
@@ -261,7 +261,7 @@ func (h *ResidenceHandler) JoinWithCode(c echo.Context) error {
return err 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 { if err != nil {
return err return err
} }
@@ -281,7 +281,7 @@ func (h *ResidenceHandler) GetResidenceUsers(c echo.Context) error {
return apperrors.BadRequest("error.invalid_residence_id") 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 { if err != nil {
return err return err
} }
@@ -306,7 +306,7 @@ func (h *ResidenceHandler) RemoveResidenceUser(c echo.Context) error {
return apperrors.BadRequest("error.invalid_user_id") 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 { if err != nil {
return err return err
} }
@@ -316,7 +316,7 @@ func (h *ResidenceHandler) RemoveResidenceUser(c echo.Context) error {
// GetResidenceTypes handles GET /api/residences/types/ // GetResidenceTypes handles GET /api/residences/types/
func (h *ResidenceHandler) GetResidenceTypes(c echo.Context) error { func (h *ResidenceHandler) GetResidenceTypes(c echo.Context) error {
types, err := h.residenceService.GetResidenceTypes() types, err := h.residenceService.GetResidenceTypes(c.Request().Context())
if err != nil { if err != nil {
return err return err
} }
@@ -348,7 +348,7 @@ func (h *ResidenceHandler) GenerateTasksReport(c echo.Context) error {
c.Bind(&req) c.Bind(&req)
// Generate the report data // 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 { if err != nil {
return err return err
} }
+3 -2
View File
@@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@@ -324,7 +325,7 @@ func TestResidenceHandler_JoinWithCode(t *testing.T) {
userRepo := repositories.NewUserRepository(db) userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{} cfg := &config.Config{}
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg) 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 := e.Group("/api/residences")
authGroup.Use(testutil.MockAuthMiddleware(newUser)) 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) { t.Run("owner tries to join own residence", func(t *testing.T) {
// Generate new code // Generate new code
shareResp2, _ := residenceService.GenerateShareCode(residence.ID, owner.ID, 24) shareResp2, _ := residenceService.GenerateShareCode(context.Background(), residence.ID, owner.ID, 24)
req := requests.JoinWithCodeRequest{ req := requests.JoinWithCodeRequest{
Code: shareResp2.ShareCode.Code, Code: shareResp2.ShareCode.Code,
+4 -4
View File
@@ -86,22 +86,22 @@ func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
} }
// Cache miss - fetch all data from services // Cache miss - fetch all data from services
residenceTypes, err := h.residenceService.GetResidenceTypes() residenceTypes, err := h.residenceService.GetResidenceTypes(c.Request().Context())
if err != nil { if err != nil {
return err return err
} }
taskCategories, err := h.taskService.GetCategories() taskCategories, err := h.taskService.GetCategories(c.Request().Context())
if err != nil { if err != nil {
return err return err
} }
taskPriorities, err := h.taskService.GetPriorities() taskPriorities, err := h.taskService.GetPriorities(c.Request().Context())
if err != nil { if err != nil {
return err return err
} }
taskFrequencies, err := h.taskService.GetFrequencies() taskFrequencies, err := h.taskService.GetFrequencies(c.Request().Context())
if err != nil { if err != nil {
return err return err
} }
+20 -20
View File
@@ -83,7 +83,7 @@ func (h *TaskHandler) GetTask(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -144,7 +144,7 @@ func (h *TaskHandler) CreateTask(c echo.Context) error {
return err 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 { if err != nil {
return err return err
} }
@@ -169,7 +169,7 @@ func (h *TaskHandler) BulkCreateTasks(c echo.Context) error {
return err 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 { if err != nil {
return err return err
} }
@@ -197,7 +197,7 @@ func (h *TaskHandler) UpdateTask(c echo.Context) error {
return err 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 { if err != nil {
return err return err
} }
@@ -215,7 +215,7 @@ func (h *TaskHandler) DeleteTask(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -235,7 +235,7 @@ func (h *TaskHandler) MarkInProgress(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -255,7 +255,7 @@ func (h *TaskHandler) CancelTask(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -275,7 +275,7 @@ func (h *TaskHandler) UncancelTask(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -295,7 +295,7 @@ func (h *TaskHandler) ArchiveTask(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -315,7 +315,7 @@ func (h *TaskHandler) UnarchiveTask(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -334,7 +334,7 @@ func (h *TaskHandler) QuickComplete(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -354,7 +354,7 @@ func (h *TaskHandler) GetTaskCompletions(c echo.Context) error {
return apperrors.BadRequest("error.invalid_task_id") 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 { if err != nil {
return err return err
} }
@@ -367,7 +367,7 @@ func (h *TaskHandler) ListCompletions(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
response, err := h.taskService.ListCompletions(user.ID) response, err := h.taskService.ListCompletions(c.Request().Context(), user.ID)
if err != nil { if err != nil {
return err return err
} }
@@ -385,7 +385,7 @@ func (h *TaskHandler) GetCompletion(c echo.Context) error {
return apperrors.BadRequest("error.invalid_completion_id") 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 { if err != nil {
return err return err
} }
@@ -465,7 +465,7 @@ func (h *TaskHandler) CreateCompletion(c echo.Context) error {
return err 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 { if err != nil {
return err return err
} }
@@ -491,7 +491,7 @@ func (h *TaskHandler) UpdateCompletion(c echo.Context) error {
return err 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 { if err != nil {
return err return err
} }
@@ -509,7 +509,7 @@ func (h *TaskHandler) DeleteCompletion(c echo.Context) error {
return apperrors.BadRequest("error.invalid_completion_id") 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 { if err != nil {
return err return err
} }
@@ -520,7 +520,7 @@ func (h *TaskHandler) DeleteCompletion(c echo.Context) error {
// GetCategories handles GET /api/tasks/categories/ // GetCategories handles GET /api/tasks/categories/
func (h *TaskHandler) GetCategories(c echo.Context) error { func (h *TaskHandler) GetCategories(c echo.Context) error {
categories, err := h.taskService.GetCategories() categories, err := h.taskService.GetCategories(c.Request().Context())
if err != nil { if err != nil {
return err return err
} }
@@ -529,7 +529,7 @@ func (h *TaskHandler) GetCategories(c echo.Context) error {
// GetPriorities handles GET /api/tasks/priorities/ // GetPriorities handles GET /api/tasks/priorities/
func (h *TaskHandler) GetPriorities(c echo.Context) error { func (h *TaskHandler) GetPriorities(c echo.Context) error {
priorities, err := h.taskService.GetPriorities() priorities, err := h.taskService.GetPriorities(c.Request().Context())
if err != nil { if err != nil {
return err return err
} }
@@ -538,7 +538,7 @@ func (h *TaskHandler) GetPriorities(c echo.Context) error {
// GetFrequencies handles GET /api/tasks/frequencies/ // GetFrequencies handles GET /api/tasks/frequencies/
func (h *TaskHandler) GetFrequencies(c echo.Context) error { func (h *TaskHandler) GetFrequencies(c echo.Context) error {
frequencies, err := h.taskService.GetFrequencies() frequencies, err := h.taskService.GetFrequencies(c.Request().Context())
if err != nil { if err != nil {
return err return err
} }
+54 -53
View File
@@ -1,6 +1,7 @@
package services package services
import ( import (
"context"
"errors" "errors"
"time" "time"
@@ -60,9 +61,9 @@ func (s *ResidenceService) SetSubscriptionService(subService *SubscriptionServic
// GetResidence gets a residence by ID with access check. // GetResidence gets a residence by ID with access check.
// The `now` parameter is used for timezone-aware completion summary aggregation. // 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.residence_not_found") 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) // Attach completion summary (honeycomb grid data)
if s.taskRepo != nil { 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 { if err != nil {
log.Warn().Err(err).Uint("residence_id", residenceID).Msg("Failed to fetch completion summary") log.Warn().Err(err).Uint("residence_id", residenceID).Msg("Failed to fetch completion summary")
} else { } else {
@@ -94,8 +95,8 @@ func (s *ResidenceService) GetResidence(residenceID, userID uint, now time.Time)
} }
// ListResidences lists all residences accessible to a user // ListResidences lists all residences accessible to a user
func (s *ResidenceService) ListResidences(userID uint) ([]responses.ResidenceResponse, error) { func (s *ResidenceService) ListResidences(ctx context.Context, userID uint) ([]responses.ResidenceResponse, error) {
residences, err := s.residenceRepo.FindByUser(userID) residences, err := s.residenceRepo.WithContext(ctx).FindByUser(userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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 // NOTE: Summary statistics (TotalTasks, TotalOverdue, etc.) are calculated client-side
// from kanban data for performance. Only per-residence OverdueCount is returned from the server. // 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) { func (s *ResidenceService) GetMyResidences(ctx context.Context, userID uint, now time.Time) (*responses.MyResidencesResponse, error) {
residences, err := s.residenceRepo.FindByUser(userID) residences, err := s.residenceRepo.WithContext(ctx).FindByUser(userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -124,7 +125,7 @@ func (s *ResidenceService) GetMyResidences(userID uint, now time.Time) (*respons
residenceIDs[i] = r.ID 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 { if err == nil && overdueCounts != nil {
for i := range residenceResponses { for i := range residenceResponses {
if count, ok := overdueCounts[residenceResponses[i].ID]; ok { 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 // 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 { if err != nil {
log.Warn().Err(err).Msg("Failed to fetch batch completion summaries") log.Warn().Err(err).Msg("Failed to fetch batch completion summaries")
} else { } 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. // DEPRECATED: Summary statistics are now calculated client-side from kanban data.
// This endpoint only returns TotalResidences; other fields will be zero. // This endpoint only returns TotalResidences; other fields will be zero.
// Clients should use calculateSummaryFromKanban() instead. // 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) // Get residence IDs (lightweight - no preloads)
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID) residenceIDs, err := s.residenceRepo.WithContext(ctx).FindResidenceIDsByUser(userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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 // 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) // Check subscription tier limits (if subscription service is wired up)
if s.subscriptionService != nil { if s.subscriptionService != nil {
if err := s.subscriptionService.CheckLimit(ownerID, "properties"); err != 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 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) return nil, apperrors.Internal(err)
} }
// Reload with relations // Reload with relations
residence, err := s.residenceRepo.FindByID(residence.ID) residence, err := s.residenceRepo.WithContext(ctx).FindByID(residence.ID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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 // 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 // Check ownership
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID) isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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 err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.residence_not_found") 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 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) return nil, apperrors.Internal(err)
} }
// Reload with relations // Reload with relations
residence, err = s.residenceRepo.FindByID(residence.ID) residence, err = s.residenceRepo.WithContext(ctx).FindByID(residence.ID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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 // 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 // Check ownership
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID) isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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) 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 // 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 // Check ownership
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID) isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -447,7 +448,7 @@ func (s *ResidenceService) GenerateShareCode(residenceID, userID uint, expiresIn
expiresInHours = 24 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 { if err != nil {
return nil, apperrors.Internal(err) 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) // 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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 { if err != nil {
return nil, apperrors.Internal(err) 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 // 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) // 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 { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -493,13 +494,13 @@ func (s *ResidenceService) GenerateSharePackage(residenceID, userID uint, expire
} }
// Get residence details for the package // Get residence details for the package
residence, err := s.residenceRepo.FindByID(residenceID) residence, err := s.residenceRepo.WithContext(ctx).FindByID(residenceID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
// Get the user who's sharing // Get the user who's sharing
user, err := s.userRepo.FindByID(userID) user, err := s.userRepo.WithContext(ctx).FindByID(userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -510,7 +511,7 @@ func (s *ResidenceService) GenerateSharePackage(residenceID, userID uint, expire
} }
// Generate the share code // 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 { if err != nil {
return nil, apperrors.Internal(err) 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 // 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 // Find the share code
shareCode, err := s.residenceRepo.FindShareCodeByCode(code) shareCode, err := s.residenceRepo.WithContext(ctx).FindShareCodeByCode(code)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.share_code_invalid") 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 // 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 { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -544,19 +545,19 @@ func (s *ResidenceService) JoinWithCode(code string, userID uint) (*responses.Jo
} }
// Add user to residence // 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) return nil, apperrors.Internal(err)
} }
// Mark share code as used (one-time use) // 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 // 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 // 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") log.Error().Err(err).Uint("code_id", shareCode.ID).Msg("Failed to deactivate share code after join")
} }
// Get the residence with full details // 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 { if err != nil {
return nil, apperrors.Internal(err) 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 // 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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 { if err != nil {
return nil, apperrors.Internal(err) 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) // 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 // Check ownership
isOwner, err := s.residenceRepo.IsOwner(residenceID, requestingUserID) isOwner, err := s.residenceRepo.WithContext(ctx).IsOwner(residenceID, requestingUserID)
if err != nil { if err != nil {
return apperrors.Internal(err) return apperrors.Internal(err)
} }
@@ -612,7 +613,7 @@ func (s *ResidenceService) RemoveUser(residenceID, userIDToRemove, requestingUse
} }
// Check if the residence exists // Check if the residence exists
residence, err := s.residenceRepo.FindByIDSimple(residenceID) residence, err := s.residenceRepo.WithContext(ctx).FindByIDSimple(residenceID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return apperrors.NotFound("error.residence_not_found") 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") 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) return apperrors.Internal(err)
} }
@@ -633,8 +634,8 @@ func (s *ResidenceService) RemoveUser(residenceID, userIDToRemove, requestingUse
} }
// GetResidenceTypes returns all residence types // GetResidenceTypes returns all residence types
func (s *ResidenceService) GetResidenceTypes() ([]responses.ResidenceTypeResponse, error) { func (s *ResidenceService) GetResidenceTypes(ctx context.Context) ([]responses.ResidenceTypeResponse, error) {
types, err := s.residenceRepo.GetAllResidenceTypes() types, err := s.residenceRepo.WithContext(ctx).GetAllResidenceTypes()
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -674,9 +675,9 @@ type TasksReportResponse struct {
} }
// GenerateTasksReport generates a report of all tasks for a residence // 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -685,7 +686,7 @@ func (s *ResidenceService) GenerateTasksReport(residenceID, userID uint) (*Tasks
} }
// Get residence details // Get residence details
residence, err := s.residenceRepo.FindByIDSimple(residenceID) residence, err := s.residenceRepo.WithContext(ctx).FindByIDSimple(residenceID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.residence_not_found") 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 // Get all tasks for the residence
tasks, err := s.residenceRepo.GetTasksForReport(residenceID) tasks, err := s.residenceRepo.WithContext(ctx).GetTasksForReport(residenceID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
+53 -52
View File
@@ -1,6 +1,7 @@
package services package services
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"testing" "testing"
@@ -44,7 +45,7 @@ func TestResidenceService_CreateResidence(t *testing.T) {
PostalCode: "78701", PostalCode: "78701",
} }
resp, err := service.CreateResidence(req, user.ID) resp, err := service.CreateResidence(context.Background(), req, user.ID)
require.NoError(t, err) require.NoError(t, err)
assert.NotNil(t, resp) assert.NotNil(t, resp)
assert.Equal(t, "Test House", resp.Data.Name) assert.Equal(t, "Test House", resp.Data.Name)
@@ -82,7 +83,7 @@ func TestResidenceService_CreateResidence_WithOptionalFields(t *testing.T) {
IsPrimary: &isPrimary, IsPrimary: &isPrimary,
} }
resp, err := service.CreateResidence(req, user.ID) resp, err := service.CreateResidence(context.Background(), req, user.ID)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "Canada", resp.Data.Country) assert.Equal(t, "Canada", resp.Data.Country)
assert.Equal(t, 3, *resp.Data.Bedrooms) 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") user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") 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) require.NoError(t, err)
assert.Equal(t, residence.ID, resp.ID) assert.Equal(t, residence.ID, resp.ID)
assert.Equal(t, "Test House", resp.Name) 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") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") 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") 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") 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) 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 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2") 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) require.NoError(t, err)
assert.Len(t, resp, 2) assert.Len(t, resp, 2)
} }
@@ -169,7 +170,7 @@ func TestResidenceService_UpdateResidence(t *testing.T) {
City: &newCity, 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) require.NoError(t, err)
assert.Equal(t, "Updated Name", resp.Data.Name) assert.Equal(t, "Updated Name", resp.Data.Name)
assert.Equal(t, "Dallas", resp.Data.City) assert.Equal(t, "Dallas", resp.Data.City)
@@ -192,7 +193,7 @@ func TestResidenceService_UpdateResidence_NotOwner(t *testing.T) {
newName := "Updated" newName := "Updated"
req := &requests.UpdateResidenceRequest{Name: &newName} 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") 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") user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") 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) require.NoError(t, err)
// Should not be found // 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) assert.Error(t, err)
} }
@@ -226,7 +227,7 @@ func TestResidenceService_DeleteResidence_NotOwner(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, sharedUser.ID) 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") 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") user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") 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) require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode.Code) assert.NotEmpty(t, resp.ShareCode.Code)
assert.Len(t, resp.ShareCode.Code, 6) 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") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
// Generate share code // 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) require.NoError(t, err)
// Join with code // 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) require.NoError(t, err)
assert.Equal(t, residence.ID, joinResp.Residence.ID) 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") owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") 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 // 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") 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") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, user1.ID) 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) require.NoError(t, err)
assert.Len(t, users, 2) // owner + shared user 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") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, sharedUser.ID) 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) require.NoError(t, err)
hasAccess, _ := residenceRepo.HasAccess(residence.ID, sharedUser.ID) 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") owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") 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") testutil.AssertAppError(t, err, http.StatusBadRequest, "error.cannot_remove_owner")
} }
@@ -389,7 +390,7 @@ func TestCreateResidence_FreeTier_EnforcesLimit(t *testing.T) {
StateProvince: "TX", StateProvince: "TX",
PostalCode: "78701", PostalCode: "78701",
} }
resp, err := service.CreateResidence(req, owner.ID) resp, err := service.CreateResidence(context.Background(), req, owner.ID)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "First House", resp.Data.Name) assert.Equal(t, "First House", resp.Data.Name)
@@ -401,7 +402,7 @@ func TestCreateResidence_FreeTier_EnforcesLimit(t *testing.T) {
StateProvince: "TX", StateProvince: "TX",
PostalCode: "78702", 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") testutil.AssertAppError(t, err, http.StatusForbidden, "error.properties_limit_exceeded")
} }
@@ -446,7 +447,7 @@ func TestCreateResidence_ProTier_AllowsMore(t *testing.T) {
StateProvince: "TX", StateProvince: "TX",
PostalCode: "78701", 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) 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) 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 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2") 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) require.NoError(t, err)
assert.Len(t, resp.Residences, 2) 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") 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) require.NoError(t, err)
assert.Empty(t, resp.Residences) 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 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2") 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) require.NoError(t, err)
assert.Equal(t, 2, resp.TotalResidences) 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") 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) require.NoError(t, err)
assert.Equal(t, 0, resp.TotalResidences) 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") user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") 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) require.NoError(t, err)
assert.Nil(t, resp) // No active code 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") other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") 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") 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") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, shared.ID) 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") 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") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
// Pass 0 hours — should default to 24 // 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) require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode.Code) 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") user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") 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) require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode) assert.NotEmpty(t, resp.ShareCode)
assert.Equal(t, "Test House", resp.ResidenceName) 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") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, shared.ID) 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") 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") 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") 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) residenceRepo.AddUser(residence.ID, shared.ID)
// shared user tries to remove other — should fail because shared is not owner // 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") 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") other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") 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") testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied")
} }
@@ -684,7 +685,7 @@ func TestResidenceService_GetResidenceTypes(t *testing.T) {
cfg := &config.Config{} cfg := &config.Config{}
service := NewResidenceService(residenceRepo, userRepo, cfg) service := NewResidenceService(residenceRepo, userRepo, cfg)
resp, err := service.GetResidenceTypes() resp, err := service.GetResidenceTypes(context.Background())
require.NoError(t, err) require.NoError(t, err)
// SeedLookupData creates 4 residence types // SeedLookupData creates 4 residence types
assert.Len(t, resp, 4) assert.Len(t, resp, 4)
@@ -711,7 +712,7 @@ func TestResidenceService_UpdateResidence_HomeProfileFields(t *testing.T) {
HeatingType: &heatingType, 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) require.NoError(t, err)
assert.True(t, resp.Data.HasPool) assert.True(t, resp.Data.HasPool)
assert.True(t, resp.Data.HasGarage) assert.True(t, resp.Data.HasGarage)
@@ -740,7 +741,7 @@ func TestResidenceService_CreateResidence_HomeProfileFields(t *testing.T) {
HasSeptic: &hasSeptic, HasSeptic: &hasSeptic,
} }
resp, err := service.CreateResidence(req, user.ID) resp, err := service.CreateResidence(context.Background(), req, user.ID)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, resp.Data.HasPool) assert.True(t, resp.Data.HasPool)
assert.True(t, resp.Data.HasSeptic) 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") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, shared.ID) 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) require.NoError(t, err)
assert.Equal(t, "Test House", resp.Name) 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 1")
testutil.CreateTestResidence(t, db, user.ID, "House 2") 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) require.NoError(t, err)
assert.Len(t, resp.Residences, 2) 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") user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") 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) require.NoError(t, err)
assert.Equal(t, "Test House", resp.Name) 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") user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") 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) require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode.Code) 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") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
// Pass 0 hours — should default to 24 // 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) require.NoError(t, err)
assert.NotEmpty(t, resp.ShareCode) assert.NotEmpty(t, resp.ShareCode)
assert.Equal(t, "Test House", resp.ResidenceName) 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 // Try removing the owner (by residence.OwnerID) — even though requestingUserID != userIDToRemove
// The second check (userIDToRemove == residence.OwnerID) should catch this // 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") 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 1")
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 2") 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) require.NoError(t, err)
assert.Equal(t, residence.ID, report.ResidenceID) assert.Equal(t, residence.ID, report.ResidenceID)
assert.Equal(t, "Test House", report.ResidenceName) 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") other := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House") 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") 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") user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
// Non-existent residence — user has no access // 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) assert.Error(t, err)
} }
@@ -926,11 +927,11 @@ func TestResidenceService_GetShareCode_WithActiveCode(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
// Generate a share code first // 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) require.NoError(t, err)
// Now get the active code // 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) require.NoError(t, err)
assert.NotNil(t, resp) assert.NotNil(t, resp)
assert.NotEmpty(t, resp.Code) assert.NotEmpty(t, resp.Code)
@@ -970,7 +971,7 @@ func TestResidenceService_CreateResidence_AllBooleanFields(t *testing.T) {
HasAttic: &hasAttic, HasAttic: &hasAttic,
} }
resp, err := service.CreateResidence(req, user.ID) resp, err := service.CreateResidence(context.Background(), req, user.ID)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, resp.Data.HasPool) assert.True(t, resp.Data.HasPool)
assert.True(t, resp.Data.HasSprinklerSystem) assert.True(t, resp.Data.HasSprinklerSystem)
@@ -1047,7 +1048,7 @@ func TestResidenceService_UpdateResidence_AllOptionalFields(t *testing.T) {
LandscapingType: &landscapingType, 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) require.NoError(t, err)
assert.Equal(t, "456 New St", resp.Data.StreetAddress) assert.Equal(t, "456 New St", resp.Data.StreetAddress)
assert.Equal(t, "CA", resp.Data.StateProvince) 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") 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) require.NoError(t, err)
assert.Empty(t, resp) assert.Empty(t, resp)
} }
+97 -97
View File
@@ -79,8 +79,8 @@ func (s *TaskService) getSummaryForUser(_ uint) responses.TotalSummary {
// === Task CRUD === // === Task CRUD ===
// GetTask gets a task by ID with access check // GetTask gets a task by ID with access check
func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, error) { func (s *TaskService) GetTask(ctx context.Context, taskID, userID uint) (*responses.TaskResponse, error) {
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") 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 // 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 { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -164,9 +164,9 @@ func (s *TaskService) GetTasksByResidence(ctx context.Context, residenceID, user
// CreateTask creates a new task. // CreateTask creates a new task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. // 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 // Check residence access
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(req.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -193,12 +193,12 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, n
TaskTemplateID: req.TemplateID, 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) return nil, apperrors.Internal(err)
} }
// Reload with relations // Reload with relations
task, err = s.taskRepo.FindByID(task.ID) task, err = s.taskRepo.WithContext(ctx).FindByID(task.ID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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 // `now` should be the start of day in the user's timezone for accurate
// kanban column categorisation on the returned task list. // 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 { if len(req.Tasks) == 0 {
return nil, apperrors.BadRequest("error.task_list_empty") return nil, apperrors.BadRequest("error.task_list_empty")
} }
// Check residence access once. // 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 { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -232,7 +232,7 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user
createdIDs := make([]uint, 0, len(req.Tasks)) 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 { for i := range req.Tasks {
entry := req.Tasks[i] entry := req.Tasks[i]
// Force the residence ID to the batch-level value so clients // 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, ContractorID: entry.ContractorID,
TaskTemplateID: entry.TemplateID, 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) return fmt.Errorf("create task %d of %d: %w", i+1, len(req.Tasks), err)
} }
createdIDs = append(createdIDs, task.ID) 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. // happen outside the transaction — rows are already committed.
created := make([]responses.TaskResponse, 0, len(createdIDs)) created := make([]responses.TaskResponse, 0, len(createdIDs))
for _, id := range createdIDs { for _, id := range createdIDs {
t, ferr := s.taskRepo.FindByID(id) t, ferr := s.taskRepo.WithContext(ctx).FindByID(id)
if ferr != nil { if ferr != nil {
return nil, apperrors.Internal(ferr) return nil, apperrors.Internal(ferr)
} }
@@ -288,8 +288,8 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user
// UpdateTask updates a task. // UpdateTask updates a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. // 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) { func (s *TaskService) UpdateTask(ctx context.Context, taskID, userID uint, req *requests.UpdateTaskRequest, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") return nil, apperrors.NotFound("error.task_not_found")
@@ -298,7 +298,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
} }
// Check access // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -349,7 +349,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
task.ContractorID = req.ContractorID 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) { if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict") return nil, apperrors.Conflict("error.version_conflict")
} }
@@ -357,7 +357,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
} }
// Reload // Reload
task, err = s.taskRepo.FindByID(task.ID) task, err = s.taskRepo.WithContext(ctx).FindByID(task.ID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -369,8 +369,8 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
} }
// DeleteTask deletes a task // DeleteTask deletes a task
func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) { func (s *TaskService) DeleteTask(ctx context.Context, taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") return nil, apperrors.NotFound("error.task_not_found")
@@ -379,7 +379,7 @@ func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSumm
} }
// Check access // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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) 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. // 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. // 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) { func (s *TaskService) MarkInProgress(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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) { if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict") return nil, apperrors.Conflict("error.version_conflict")
} }
@@ -427,7 +427,7 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo
} }
// Reload // Reload
task, err = s.taskRepo.FindByID(taskID) task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -440,8 +440,8 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo
// CancelTask cancels a task. // CancelTask cancels a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. // 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) { func (s *TaskService) CancelTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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) { if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict") return nil, apperrors.Conflict("error.version_conflict")
} }
@@ -470,7 +470,7 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses
} }
// Reload // Reload
task, err = s.taskRepo.FindByID(taskID) task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -483,8 +483,8 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses
// UncancelTask uncancels a task. // UncancelTask uncancels a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. // 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) { func (s *TaskService) UncancelTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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) { if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict") return nil, apperrors.Conflict("error.version_conflict")
} }
@@ -509,7 +509,7 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons
} }
// Reload // Reload
task, err = s.taskRepo.FindByID(taskID) task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -522,8 +522,8 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons
// ArchiveTask archives a task. // ArchiveTask archives a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. // 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) { func (s *TaskService) ArchiveTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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) { if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict") return nil, apperrors.Conflict("error.version_conflict")
} }
@@ -552,7 +552,7 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response
} }
// Reload // Reload
task, err = s.taskRepo.FindByID(taskID) task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -565,8 +565,8 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response
// UnarchiveTask unarchives a task. // UnarchiveTask unarchives a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. // 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) { func (s *TaskService) UnarchiveTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") 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 // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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") 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) { if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict") return nil, apperrors.Conflict("error.version_conflict")
} }
@@ -591,7 +591,7 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon
} }
// Reload // Reload
task, err = s.taskRepo.FindByID(taskID) task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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. // CreateCompletion creates a task completion.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. // 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 // Get the task
task, err := s.taskRepo.FindByID(req.TaskID) task, err := s.taskRepo.WithContext(ctx).FindByID(req.TaskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") return nil, apperrors.NotFound("error.task_not_found")
@@ -617,7 +617,7 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
} }
// Check access // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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 // Note: Frequency is no longer preloaded for performance, so we load it separately if needed
var intervalDays *int var intervalDays *int
if task.FrequencyID != nil { 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 err == nil && frequency != nil {
if frequency.Name == "Custom" { if frequency.Name == "Custom" {
// Custom frequency - use task's custom_interval_days // 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 // 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. // in a single transaction for atomicity. If any operation fails, all are rolled back.
txErr := s.taskRepo.DB().Transaction(func(tx *gorm.DB) error { txErr := s.taskRepo.WithContext(ctx).DB().Transaction(func(tx *gorm.DB) error {
if err := s.taskRepo.CreateCompletionTx(tx, completion); err != nil { if err := s.taskRepo.WithContext(ctx).CreateCompletionTx(tx, completion); err != nil {
return err 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 err
} }
// B-07: Create images inside the same transaction as completion // 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 // 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 { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
// Reload task with updated completions (so client can update kanban column) // 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 { if err != nil {
// Non-fatal - still return the completion, just without the task // 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") 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 // 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) // Return completion with updated task (includes kanban_column for UI update)
resp := responses.NewTaskCompletionWithTaskResponseWithTime(completion, task, 30, now) 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 // LE-01: The entire operation (completion creation + task update) is wrapped in a
// transaction for atomicity. // transaction for atomicity.
// Returns only success/error, no response body. // 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 // Get the task
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return apperrors.NotFound("error.task_not_found") return apperrors.NotFound("error.task_not_found")
@@ -755,7 +755,7 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
} }
// Check access // Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
if err != nil { if err != nil {
return apperrors.Internal(err) return apperrors.Internal(err)
} }
@@ -782,7 +782,7 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
var quickIntervalDays *int var quickIntervalDays *int
var frequencyName = "unknown" var frequencyName = "unknown"
if task.FrequencyID != nil { 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 err == nil && frequency != nil {
frequencyName = frequency.Name frequencyName = frequency.Name
if frequency.Name == "Custom" { 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 // LE-01: Wrap completion creation and task update in a transaction for atomicity
txErr := s.taskRepo.DB().Transaction(func(tx *gorm.DB) error { txErr := s.taskRepo.WithContext(ctx).DB().Transaction(func(tx *gorm.DB) error {
if err := s.taskRepo.CreateCompletionTx(tx, completion); err != nil { if err := s.taskRepo.WithContext(ctx).CreateCompletionTx(tx, completion); err != nil {
return err 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 err
} }
return nil 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") 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 return nil
} }
// sendTaskCompletedNotification sends notifications when a task is completed // 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 // 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 { if err != nil {
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to get residence users for notification") log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to get residence users for notification")
return return
} }
// Get residence name // Get residence name
residence, err := s.residenceRepo.FindByIDSimple(task.ResidenceID) residence, err := s.residenceRepo.WithContext(ctx).FindByIDSimple(task.ResidenceID)
residenceName := "your property" residenceName := "your property"
if err == nil && residence != nil { if err == nil && residence != nil {
residenceName = residence.Name residenceName = residence.Name
@@ -1000,8 +1000,8 @@ func (s *TaskService) getContentTypeFromPath(path string) string {
} }
// GetCompletion gets a task completion by ID // GetCompletion gets a task completion by ID
func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskCompletionResponse, error) { func (s *TaskService) GetCompletion(ctx context.Context, completionID, userID uint) (*responses.TaskCompletionResponse, error) {
completion, err := s.taskRepo.FindCompletionByID(completionID) completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.completion_not_found") 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 // 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 { if err != nil {
return nil, apperrors.Internal(err) 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 // 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) // Get all residence IDs (lightweight - no preloads)
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID) residenceIDs, err := s.residenceRepo.WithContext(ctx).FindResidenceIDsByUser(userID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1034,7 +1034,7 @@ func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionRe
return []responses.TaskCompletionResponse{}, nil return []responses.TaskCompletionResponse{}, nil
} }
completions, err := s.taskRepo.FindCompletionsByUser(userID, residenceIDs) completions, err := s.taskRepo.WithContext(ctx).FindCompletionsByUser(userID, residenceIDs)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1043,8 +1043,8 @@ func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionRe
} }
// UpdateCompletion updates an existing task completion // UpdateCompletion updates an existing task completion
func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.UpdateTaskCompletionRequest) (*responses.TaskCompletionResponse, error) { func (s *TaskService) UpdateCompletion(ctx context.Context, completionID, userID uint, req *requests.UpdateTaskCompletionRequest) (*responses.TaskCompletionResponse, error) {
completion, err := s.taskRepo.FindCompletionByID(completionID) completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.completion_not_found") 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 // 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 { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1072,7 +1072,7 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.
completion.Rating = req.Rating 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) return nil, apperrors.Internal(err)
} }
@@ -1082,13 +1082,13 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.
CompletionID: completion.ID, CompletionID: completion.ID,
ImageURL: imageURL, 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") log.Error().Err(err).Uint("completion_id", completion.ID).Msg("Failed to create completion image during update")
} }
} }
// Reload to get full associations // Reload to get full associations
updated, err := s.taskRepo.FindCompletionByID(completionID) updated, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) 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: // P1-7: After deleting a completion, NextDueDate must be recalculated:
// - If no completions remain: restore NextDueDate = DueDate (original schedule) // - If no completions remain: restore NextDueDate = DueDate (original schedule)
// - If completions remain (recurring): recalculate from latest remaining completion + frequency days // - If completions remain (recurring): recalculate from latest remaining completion + frequency days
func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) { func (s *TaskService) DeleteCompletion(ctx context.Context, completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
completion, err := s.taskRepo.FindCompletionByID(completionID) completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.completion_not_found") return nil, apperrors.NotFound("error.completion_not_found")
@@ -1112,7 +1112,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
} }
// Check access // 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 { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1122,12 +1122,12 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
taskID := completion.TaskID 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) return nil, apperrors.Internal(err)
} }
// Recalculate NextDueDate based on remaining completions // Recalculate NextDueDate based on remaining completions
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
// Non-fatal for the delete operation itself, but log the error // 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") 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 // Get remaining completions for this task
remainingCompletions, err := s.taskRepo.FindCompletionsByTask(taskID) remainingCompletions, err := s.taskRepo.WithContext(ctx).FindCompletionsByTask(taskID)
if err != nil { if err != nil {
log.Error().Err(err).Uint("task_id", taskID).Msg("Failed to query remaining completions after deletion") log.Error().Err(err).Uint("task_id", taskID).Msg("Failed to query remaining completions after deletion")
return &responses.DeleteWithSummaryResponse{ return &responses.DeleteWithSummaryResponse{
@@ -1150,7 +1150,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
// Determine the task's frequency interval // Determine the task's frequency interval
var intervalDays *int var intervalDays *int
if task.FrequencyID != nil { 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 freqErr == nil && frequency != nil {
if frequency.Name == "Custom" { if frequency.Name == "Custom" {
intervalDays = task.CustomIntervalDays intervalDays = task.CustomIntervalDays
@@ -1175,7 +1175,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
task.NextDueDate = nil 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") 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 // 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 // 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 // Get the task to check access
task, err := s.taskRepo.FindByID(taskID) task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperrors.NotFound("error.task_not_found") 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 // 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 { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1207,7 +1207,7 @@ func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.Tas
} }
// Get completions for the task // Get completions for the task
completions, err := s.taskRepo.FindCompletionsByTask(taskID) completions, err := s.taskRepo.WithContext(ctx).FindCompletionsByTask(taskID)
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1218,8 +1218,8 @@ func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.Tas
// === Lookups === // === Lookups ===
// GetCategories returns all task categories // GetCategories returns all task categories
func (s *TaskService) GetCategories() ([]responses.TaskCategoryResponse, error) { func (s *TaskService) GetCategories(ctx context.Context) ([]responses.TaskCategoryResponse, error) {
categories, err := s.taskRepo.GetAllCategories() categories, err := s.taskRepo.WithContext(ctx).GetAllCategories()
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1232,8 +1232,8 @@ func (s *TaskService) GetCategories() ([]responses.TaskCategoryResponse, error)
} }
// GetPriorities returns all task priorities // GetPriorities returns all task priorities
func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error) { func (s *TaskService) GetPriorities(ctx context.Context) ([]responses.TaskPriorityResponse, error) {
priorities, err := s.taskRepo.GetAllPriorities() priorities, err := s.taskRepo.WithContext(ctx).GetAllPriorities()
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1246,8 +1246,8 @@ func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error)
} }
// GetFrequencies returns all task frequencies // GetFrequencies returns all task frequencies
func (s *TaskService) GetFrequencies() ([]responses.TaskFrequencyResponse, error) { func (s *TaskService) GetFrequencies(ctx context.Context) ([]responses.TaskFrequencyResponse, error) {
frequencies, err := s.taskRepo.GetAllFrequencies() frequencies, err := s.taskRepo.WithContext(ctx).GetAllFrequencies()
if err != nil { if err != nil {
return nil, apperrors.Internal(err) return nil, apperrors.Internal(err)
} }
@@ -1,6 +1,7 @@
package services package services
import ( import (
"context"
"testing" "testing"
"time" "time"
@@ -52,7 +53,7 @@ func TestTaskService_CompleteToCancel_OneTimeTask(t *testing.T) {
TaskID: task.ID, TaskID: task.ID,
Notes: "Completed", Notes: "Completed",
} }
_, err = service.CreateCompletion(completionReq, user.ID, now) _, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// Verify task is completed (NextDueDate=nil) // 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") assert.Equal(t, "completed_tasks", column, "Completed one-time task should be in completed column")
// Step 2: Cancel the completed task // 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) require.NoError(t, err)
assert.True(t, cancelResp.Data.IsCancelled, "Task should be cancelled") 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) now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Step 1: Cancel the task // 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) require.NoError(t, err)
assert.True(t, cancelResp.Data.IsCancelled) assert.True(t, cancelResp.Data.IsCancelled)
@@ -115,7 +116,7 @@ func TestTaskService_CancelToComplete(t *testing.T) {
TaskID: task.ID, TaskID: task.ID,
Notes: "Completed even though cancelled", 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) require.NoError(t, err)
assert.NotZero(t, completionResp.Data.ID, "Completion should be created") 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) now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Step 1: Archive the task // 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) require.NoError(t, err)
assert.True(t, archiveResp.Data.IsArchived) assert.True(t, archiveResp.Data.IsArchived)
@@ -169,7 +170,7 @@ func TestTaskService_ArchiveToComplete(t *testing.T) {
TaskID: task.ID, TaskID: task.ID,
Notes: "Completed even though archived", 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) require.NoError(t, err)
assert.NotZero(t, completionResp.Data.ID, "Completion should be created") assert.NotZero(t, completionResp.Data.ID, "Completion should be created")
@@ -217,11 +218,11 @@ func TestTaskService_CompleteToArchive_OneTimeTask(t *testing.T) {
TaskID: task.ID, TaskID: task.ID,
Notes: "Done", Notes: "Done",
} }
_, err = service.CreateCompletion(completionReq, user.ID, now) _, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// Step 2: Archive the completed task // 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) require.NoError(t, err)
assert.True(t, archiveResp.Data.IsArchived, "Task should be archived") 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) now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Step 1: Mark in progress // 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) require.NoError(t, err)
assert.True(t, inProgressResp.Data.InProgress) 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") assert.Equal(t, "in_progress_tasks", column, "Task should be in in_progress column")
// Step 2: Cancel the in-progress task // 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) require.NoError(t, err)
assert.True(t, cancelResp.Data.IsCancelled) 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") assert.Equal(t, "cancelled_tasks", column, "Cancelled task should be in cancelled column")
// Step 3: Uncancel the task // 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) require.NoError(t, err)
assert.False(t, uncancelResp.Data.IsCancelled) 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) now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Cycle 1: Cancel -> Uncancel // 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) 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) require.NoError(t, err)
// Cycle 2: Cancel -> Uncancel // 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) 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) require.NoError(t, err)
// Verify final state // Verify final state
@@ -393,7 +394,7 @@ func TestTaskService_CompleteToMarkInProgress_OneTimeTask(t *testing.T) {
TaskID: task.ID, TaskID: task.ID,
Notes: "Done", Notes: "Done",
} }
_, err = service.CreateCompletion(completionReq, user.ID, now) _, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// Verify completed state // Verify completed state
@@ -402,7 +403,7 @@ func TestTaskService_CompleteToMarkInProgress_OneTimeTask(t *testing.T) {
assert.Nil(t, taskAfterComplete.NextDueDate) assert.Nil(t, taskAfterComplete.NextDueDate)
// Step 2: Mark in progress // 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) require.NoError(t, err)
assert.True(t, ipResp.Data.InProgress, "InProgress should be true") 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) now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Step 1: Mark in progress // 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) require.NoError(t, err)
assert.True(t, ipResp.Data.InProgress) assert.True(t, ipResp.Data.InProgress)
@@ -469,7 +470,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) {
Notes: "Week 1 done", Notes: "Week 1 done",
CompletedAt: &firstCompletedAt, CompletedAt: &firstCompletedAt,
} }
_, err = service.CreateCompletion(completionReq1, user.ID, now) _, err = service.CreateCompletion(context.Background(), completionReq1, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// Verify: InProgress reset to false, NextDueDate recalculated // 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") assert.NotEqual(t, "completed_tasks", column, "Recurring task should not be in completed column")
// Step 3: Mark in progress again for next cycle // 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) require.NoError(t, err)
assert.True(t, ipResp2.Data.InProgress) assert.True(t, ipResp2.Data.InProgress)
@@ -498,7 +499,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) {
Notes: "Week 2 done", Notes: "Week 2 done",
CompletedAt: &secondCompletedAt, CompletedAt: &secondCompletedAt,
} }
_, err = service.CreateCompletion(completionReq2, user.ID, now) _, err = service.CreateCompletion(context.Background(), completionReq2, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// Verify: InProgress reset again, NextDueDate recalculated from second completion // 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") assert.Equal(t, "due_soon_tasks", column, "Task due in 5 days should be due_soon")
// Step 1: Archive // 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) require.NoError(t, err)
assert.True(t, archiveResp.Data.IsArchived) 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") assert.Equal(t, "cancelled_tasks", column, "Archived task should be in cancelled column")
// Step 2: Unarchive // 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) require.NoError(t, err)
assert.False(t, unarchiveResp.Data.IsArchived) assert.False(t, unarchiveResp.Data.IsArchived)
@@ -738,7 +739,7 @@ func TestTaskService_OptimisticLocking_UpdateWithCorrectVersion(t *testing.T) {
Title: &newTitle, Title: &newTitle,
} }
now := time.Now().UTC() 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) require.NoError(t, err)
assert.Equal(t, "Updated Title", resp.Data.Title) 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 // Reset version and verify service cancel succeeds with correct version
db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 1) 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) require.NoError(t, err)
assert.True(t, cancelResp.Data.IsCancelled, "Service cancel should succeed with correct version") assert.True(t, cancelResp.Data.IsCancelled, "Service cancel should succeed with correct version")
} }
+110 -110
View File
@@ -43,7 +43,7 @@ func TestTaskService_CreateTask(t *testing.T) {
} }
now := time.Now().UTC() 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.NoError(t, err)
assert.NotZero(t, resp.Data.ID) assert.NotZero(t, resp.Data.ID)
assert.Equal(t, "Fix leaky faucet", resp.Data.Title) assert.Equal(t, "Fix leaky faucet", resp.Data.Title)
@@ -79,7 +79,7 @@ func TestTaskService_CreateTask_WithOptionalFields(t *testing.T) {
} }
now := time.Now().UTC() 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.NoError(t, err)
// Note: Category and Priority are no longer preloaded for performance // Note: Category and Priority are no longer preloaded for performance
// Client resolves from cache using CategoryID and PriorityID // Client resolves from cache using CategoryID and PriorityID
@@ -120,7 +120,7 @@ func TestTaskService_CreateTask_WithTemplateID(t *testing.T) {
Title: "From template: " + tc.name, Title: "From template: " + tc.name,
TemplateID: tc.templateID, 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) require.NoError(t, err)
if tc.wantID == nil { if tc.wantID == nil {
@@ -165,7 +165,7 @@ func TestTaskService_BulkCreateTasks(t *testing.T) {
{ResidenceID: residence.ID, Title: "Task C"}, {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) require.NoError(t, err)
assert.Equal(t, 3, resp.CreatedCount) assert.Equal(t, 3, resp.CreatedCount)
assert.Len(t, resp.Tasks, 3) assert.Len(t, resp.Tasks, 3)
@@ -198,7 +198,7 @@ func TestTaskService_BulkCreateTasks(t *testing.T) {
ResidenceID: residence.ID, ResidenceID: residence.ID,
Tasks: []requests.CreateTaskRequest{}, // empty triggers the guard 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") testutil.AssertAppError(t, err, http.StatusBadRequest, "error.task_list_empty")
var after int64 var after int64
@@ -214,7 +214,7 @@ func TestTaskService_BulkCreateTasks(t *testing.T) {
{ResidenceID: residence.ID, Title: "Sneaky"}, {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") 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"}, {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.NoError(t, err)
require.Len(t, resp.Tasks, 1) require.Len(t, resp.Tasks, 1)
assert.Equal(t, residence.ID, resp.Tasks[0].ResidenceID) assert.Equal(t, residence.ID, resp.Tasks[0].ResidenceID)
@@ -251,7 +251,7 @@ func TestTaskService_CreateTask_AccessDenied(t *testing.T) {
} }
now := time.Now().UTC() 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 // When creating a task, residence access is checked first
testutil.AssertAppError(t, err, http.StatusForbidden, "error.residence_access_denied") 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") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") 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) require.NoError(t, err)
assert.Equal(t, task.ID, resp.ID) assert.Equal(t, task.ID, resp.ID)
assert.Equal(t, "Test Task", resp.Title) 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") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, owner.ID, "Test Task") 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") testutil.AssertAppError(t, err, http.StatusForbidden, "error.task_access_denied")
} }
@@ -332,7 +332,7 @@ func TestTaskService_UpdateTask(t *testing.T) {
} }
now := time.Now().UTC() 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) require.NoError(t, err)
assert.Equal(t, "Updated Title", resp.Data.Title) assert.Equal(t, "Updated Title", resp.Data.Title)
assert.Equal(t, "Updated description", resp.Data.Description) 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") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task") 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) require.NoError(t, err)
_, err = service.GetTask(task.ID, user.ID) _, err = service.GetTask(context.Background(), task.ID, user.ID)
assert.Error(t, err) 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") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
now := time.Now().UTC() 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) require.NoError(t, err)
assert.True(t, resp.Data.IsCancelled) 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") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
now := time.Now().UTC() now := time.Now().UTC()
service.CancelTask(task.ID, user.ID, now) service.CancelTask(context.Background(), task.ID, user.ID, now)
_, err := service.CancelTask(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") 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") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
now := time.Now().UTC() now := time.Now().UTC()
service.CancelTask(task.ID, user.ID, now) service.CancelTask(context.Background(), task.ID, user.ID, now)
resp, err := service.UncancelTask(task.ID, user.ID, now) resp, err := service.UncancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
assert.False(t, resp.Data.IsCancelled) 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") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
now := time.Now().UTC() 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) require.NoError(t, err)
assert.True(t, resp.Data.IsArchived) 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") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
now := time.Now().UTC() now := time.Now().UTC()
service.ArchiveTask(task.ID, user.ID, now) service.ArchiveTask(context.Background(), task.ID, user.ID, now)
resp, err := service.UnarchiveTask(task.ID, user.ID, now) resp, err := service.UnarchiveTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
assert.False(t, resp.Data.IsArchived) 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") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
now := time.Now().UTC() 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) require.NoError(t, err)
assert.True(t, resp.Data.InProgress) assert.True(t, resp.Data.InProgress)
} }
@@ -477,7 +477,7 @@ func TestTaskService_CreateCompletion(t *testing.T) {
} }
now := time.Now().UTC() 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) require.NoError(t, err)
assert.NotZero(t, resp.Data.ID) assert.NotZero(t, resp.Data.ID)
assert.Equal(t, task.ID, resp.Data.TaskID) assert.Equal(t, task.ID, resp.Data.TaskID)
@@ -520,7 +520,7 @@ func TestTaskService_CreateCompletion_RecurringTask_ResetsInProgress(t *testing.
} }
now := time.Now().UTC() 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) require.NoError(t, err)
assert.NotZero(t, resp.Data.ID) assert.NotZero(t, resp.Data.ID)
@@ -558,7 +558,7 @@ func TestTaskService_GetCompletion(t *testing.T) {
} }
db.Create(completion) 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) require.NoError(t, err)
assert.Equal(t, completion.ID, resp.ID) assert.Equal(t, completion.ID, resp.ID)
assert.Equal(t, "Test notes", resp.Notes) assert.Equal(t, "Test notes", resp.Notes)
@@ -582,10 +582,10 @@ func TestTaskService_DeleteCompletion(t *testing.T) {
} }
db.Create(completion) db.Create(completion)
_, err := service.DeleteCompletion(completion.ID, user.ID) _, err := service.DeleteCompletion(context.Background(), completion.ID, user.ID)
require.NoError(t, err) require.NoError(t, err)
_, err = service.GetCompletion(completion.ID, user.ID) _, err = service.GetCompletion(context.Background(), completion.ID, user.ID)
assert.Error(t, err) assert.Error(t, err)
} }
@@ -623,7 +623,7 @@ func TestTaskService_CreateCompletion_TransactionIntegrity(t *testing.T) {
} }
now := time.Now().UTC() 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) require.NoError(t, err)
assert.NotZero(t, resp.Data.ID) assert.NotZero(t, resp.Data.ID)
@@ -721,7 +721,7 @@ func TestTaskService_CreateCompletion_UpdateError_ReturnedNotSwallowed(t *testin
now := time.Now().UTC() now := time.Now().UTC()
// This call will succeed because FindByID loads version=1, UpdateTx uses version=1, DB has version=1. // 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. // 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") require.NoError(t, err, "CreateCompletion should succeed with matching versions")
assert.NotZero(t, resp.Data.ID) assert.NotZero(t, resp.Data.ID)
} }
@@ -760,7 +760,7 @@ func TestTaskService_DeleteCompletion_OneTime_RestoresOriginalDueDate(t *testing
Notes: "Completed", Notes: "Completed",
} }
now := time.Now().UTC() 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) require.NoError(t, err)
// Confirm NextDueDate is nil after completion // 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") assert.Nil(t, taskAfterComplete.NextDueDate, "NextDueDate should be nil after one-time completion")
// Delete the 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) require.NoError(t, err)
// Verify NextDueDate is restored to the original DueDate // Verify NextDueDate is restored to the original DueDate
@@ -820,7 +820,7 @@ func TestTaskService_DeleteCompletion_Recurring_RecalculatesFromLastCompletion(t
CompletedAt: &firstCompletedAt, CompletedAt: &firstCompletedAt,
} }
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(firstReq, user.ID, now) _, err = service.CreateCompletion(context.Background(), firstReq, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// Second completion on Feb 15 // Second completion on Feb 15
@@ -830,7 +830,7 @@ func TestTaskService_DeleteCompletion_Recurring_RecalculatesFromLastCompletion(t
Notes: "Second completion", Notes: "Second completion",
CompletedAt: &secondCompletedAt, CompletedAt: &secondCompletedAt,
} }
resp, err := service.CreateCompletion(secondReq, user.ID, now) resp, err := service.CreateCompletion(context.Background(), secondReq, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// NextDueDate should be Feb 15 + 30 days = Mar 17 // 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()) assert.Equal(t, expectedAfterSecond.Day(), taskAfterSecond.NextDueDate.Day())
// Delete the second (latest) completion // 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) require.NoError(t, err)
// NextDueDate should be recalculated from the first completion: Jan 15 + 30 = Feb 14 // 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, CompletedAt: &completedAt,
} }
now := time.Now().UTC() 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) require.NoError(t, err)
// Verify NextDueDate was set to completedAt + 7 days // Verify NextDueDate was set to completedAt + 7 days
@@ -904,7 +904,7 @@ func TestTaskService_DeleteCompletion_LastCompletion_RestoresDueDate(t *testing.
require.NotNil(t, taskAfterComplete.NextDueDate) require.NotNil(t, taskAfterComplete.NextDueDate)
// Delete the only completion // 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) require.NoError(t, err)
// NextDueDate should be restored to original DueDate since no completions remain // 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) residenceRepo := repositories.NewResidenceRepository(db)
service := NewTaskService(taskRepo, residenceRepo) service := NewTaskService(taskRepo, residenceRepo)
categories, err := service.GetCategories() categories, err := service.GetCategories(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.Greater(t, len(categories), 0) assert.Greater(t, len(categories), 0)
@@ -941,7 +941,7 @@ func TestTaskService_GetPriorities(t *testing.T) {
residenceRepo := repositories.NewResidenceRepository(db) residenceRepo := repositories.NewResidenceRepository(db)
service := NewTaskService(taskRepo, residenceRepo) service := NewTaskService(taskRepo, residenceRepo)
priorities, err := service.GetPriorities() priorities, err := service.GetPriorities(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.Greater(t, len(priorities), 0) assert.Greater(t, len(priorities), 0)
@@ -958,7 +958,7 @@ func TestTaskService_GetFrequencies(t *testing.T) {
residenceRepo := repositories.NewResidenceRepository(db) residenceRepo := repositories.NewResidenceRepository(db)
service := NewTaskService(taskRepo, residenceRepo) service := NewTaskService(taskRepo, residenceRepo)
frequencies, err := service.GetFrequencies() frequencies, err := service.GetFrequencies(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.Greater(t, len(frequencies), 0) 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") task := testutil.CreateTestTask(t, db, residence.ID, owner.ID, "Test Task")
// Shared user should be able to see the 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) require.NoError(t, err)
assert.Equal(t, task.ID, resp.ID) assert.Equal(t, task.ID, resp.ID)
@@ -991,7 +991,7 @@ func TestTaskService_SharedUserAccess(t *testing.T) {
Title: "Shared User Task", Title: "Shared User Task",
} }
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateTask(req, sharedUser.ID, now) _, err = service.CreateTask(context.Background(), req, sharedUser.ID, now)
require.NoError(t, err) require.NoError(t, err)
} }
@@ -1017,7 +1017,7 @@ func TestTaskService_CreateTask_NextDueDateEqualsDueDate(t *testing.T) {
} }
now := time.Now().UTC() 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.NoError(t, err)
require.NotNil(t, resp.Data.DueDate, "DueDate should be set") require.NotNil(t, resp.Data.DueDate, "DueDate should be set")
require.NotNil(t, resp.Data.NextDueDate, "NextDueDate should be initialized") 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() 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.NoError(t, err)
assert.Nil(t, resp.Data.DueDate, "DueDate should be nil") assert.Nil(t, resp.Data.DueDate, "DueDate should be nil")
assert.Nil(t, resp.Data.NextDueDate, "NextDueDate should be nil when no DueDate") 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() 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.NoError(t, err)
assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved") assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved")
assert.Equal(t, weeklyFreq.ID, *resp.Data.FrequencyID, "FrequencyID should match Weekly") 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() 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.NoError(t, err)
assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved") assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved")
assert.NotNil(t, resp.Data.CustomIntervalDays, "CustomIntervalDays 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() 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.NoError(t, err)
assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved") assert.NotNil(t, resp.Data.FrequencyID, "FrequencyID should be saved")
assert.Nil(t, resp.Data.NextDueDate, "NextDueDate should be nil when no DueDate") 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() 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.NoError(t, err, "Creating task with past due date should not error")
require.NotNil(t, resp.Data.DueDate, "DueDate should be set") 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") 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() 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.NoError(t, err)
assert.True(t, resp.Data.InProgress, "InProgress should be true in response") 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() 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.NoError(t, err)
assert.Equal(t, "Full task with all fields", resp.Data.Title) 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() 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") require.NoError(t, err, "Should save even without FrequencyID")
assert.NotNil(t, resp.Data.CustomIntervalDays, "CustomIntervalDays should be saved") assert.NotNil(t, resp.Data.CustomIntervalDays, "CustomIntervalDays should be saved")
assert.Equal(t, 10, *resp.Data.CustomIntervalDays) assert.Equal(t, 10, *resp.Data.CustomIntervalDays)
@@ -1300,7 +1300,7 @@ func TestTaskService_CreateTask_InvalidResidenceAccess(t *testing.T) {
} }
now := time.Now().UTC() 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") 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() 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") require.Error(t, err, "Should error for non-existent residence")
// Should return forbidden since user has no access to 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") 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) // Complete the task (sets NextDueDate to nil)
now := time.Now().UTC() now := time.Now().UTC()
completionResp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ completionResp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, TaskID: task.ID,
Notes: "Done", Notes: "Done",
}, user.ID, now) }, user.ID, now)
@@ -1373,7 +1373,7 @@ func TestTaskService_DeleteCompletion_OneTime_RestoresAndExitsKanbanCompleted(t
"Task should be in completed state") "Task should be in completed state")
// Delete the 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) require.NoError(t, err)
// Verify NextDueDate restored to original DueDate // Verify NextDueDate restored to original DueDate
@@ -1425,7 +1425,7 @@ func TestTaskService_DeleteCompletion_Recurring_RestoresToOriginalDueDate(t *tes
// Complete on Apr 3 // Complete on Apr 3
completedAt := time.Date(2026, 4, 3, 10, 0, 0, 0, time.UTC) completedAt := time.Date(2026, 4, 3, 10, 0, 0, 0, time.UTC)
now := time.Now().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, TaskID: task.ID, Notes: "Weekly done", CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -1437,7 +1437,7 @@ func TestTaskService_DeleteCompletion_Recurring_RestoresToOriginalDueDate(t *tes
assert.Equal(t, 10, taskAfterComplete.NextDueDate.Day()) assert.Equal(t, 10, taskAfterComplete.NextDueDate.Day())
// Delete the 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) require.NoError(t, err)
// Verify NextDueDate restored to original DueDate (Apr 1) // Verify NextDueDate restored to original DueDate (Apr 1)
@@ -1483,21 +1483,21 @@ func TestTaskService_DeleteCompletion_MultipleCompletions_SequentialDeletion(t *
// Completion 1: Jan 5 // Completion 1: Jan 5
c1At := time.Date(2026, 1, 5, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, Notes: "Completion 1", CompletedAt: &c1At,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// Completion 2: Jan 12 // Completion 2: Jan 12
c2At := time.Date(2026, 1, 12, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, Notes: "Completion 2", CompletedAt: &c2At,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
// Completion 3: Jan 19 // Completion 3: Jan 19
c3At := time.Date(2026, 1, 19, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, Notes: "Completion 3", CompletedAt: &c3At,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -1509,7 +1509,7 @@ func TestTaskService_DeleteCompletion_MultipleCompletions_SequentialDeletion(t *
assert.Equal(t, 26, taskAfter3.NextDueDate.Day()) assert.Equal(t, 26, taskAfter3.NextDueDate.Day())
// Delete completion 3 (latest) -> recalc from completion 2: Jan 12 + 7 = Jan 19 // 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) require.NoError(t, err)
var taskAfterDel3 models.Task var taskAfterDel3 models.Task
db.First(&taskAfterDel3, task.ID) 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)") "NextDueDate should be Jan 19 (completion 2: Jan 12 + 7)")
// Delete completion 2 (now latest) -> recalc from completion 1: Jan 5 + 7 = Jan 12 // 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) require.NoError(t, err)
var taskAfterDel2 models.Task var taskAfterDel2 models.Task
db.First(&taskAfterDel2, task.ID) 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)") "NextDueDate should be Jan 12 (completion 1: Jan 5 + 7)")
// Delete completion 1 (last remaining) -> restore to original DueDate: Jan 1 // 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) require.NoError(t, err)
var taskAfterDel1 models.Task var taskAfterDel1 models.Task
db.First(&taskAfterDel1, task.ID) db.First(&taskAfterDel1, task.ID)
@@ -1570,19 +1570,19 @@ func TestTaskService_DeleteCompletion_MiddleCompletion_KeepsLatest(t *testing.T)
// Create 3 completions // Create 3 completions
c1At := time.Date(2026, 2, 3, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, Notes: "First", CompletedAt: &c1At,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
c2At := time.Date(2026, 2, 10, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, Notes: "Second", CompletedAt: &c2At,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
c3At := time.Date(2026, 2, 17, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, Notes: "Third", CompletedAt: &c3At,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -1594,7 +1594,7 @@ func TestTaskService_DeleteCompletion_MiddleCompletion_KeepsLatest(t *testing.T)
assert.Equal(t, 24, taskBefore.NextDueDate.Day()) assert.Equal(t, 24, taskBefore.NextDueDate.Day())
// Delete the MIDDLE completion (2nd) // 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) require.NoError(t, err)
// NextDueDate should still be based on the latest (3rd): Feb 17 + 7 = Feb 24 // 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) // Complete it (sets InProgress = false)
now := time.Now().UTC() now := time.Now().UTC()
completionResp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ completionResp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Completed", TaskID: task.ID, Notes: "Completed",
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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") assert.False(t, taskAfterComplete.InProgress, "InProgress should be false after completion")
// Delete the 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) require.NoError(t, err)
// InProgress is NOT restored by DeleteCompletion // InProgress is NOT restored by DeleteCompletion
@@ -1674,7 +1674,7 @@ func TestTaskService_TaskWithNoDates(t *testing.T) {
Title: "No Date Task", Title: "No Date Task",
} }
now := time.Now().UTC() 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) require.NoError(t, err)
// Verify dates are nil // Verify dates are nil
@@ -1686,7 +1686,7 @@ func TestTaskService_TaskWithNoDates(t *testing.T) {
"Task with no due date should be in upcoming column") "Task with no due date should be in upcoming column")
// Complete it // Complete it
completionResp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ completionResp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: createResp.Data.ID, Notes: "Done", TaskID: createResp.Data.ID, Notes: "Done",
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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") assert.False(t, task.IsOverdueAt(today), "Task due today should NOT be overdue")
// Complete it // Complete it
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Done today", TaskID: task.ID, Notes: "Done today",
}, user.ID, today) }, user.ID, today)
require.NoError(t, err) 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") assert.True(t, task.IsOverdueAt(today), "Task due yesterday should be overdue")
// Complete it // Complete it
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Done late", TaskID: task.ID, Notes: "Done late",
}, user.ID, today) }, user.ID, today)
require.NoError(t, err) require.NoError(t, err)
@@ -1802,7 +1802,7 @@ func TestTaskService_TaskVeryFarFuture(t *testing.T) {
DueDate: &requests.FlexibleDate{Time: farFuture}, DueDate: &requests.FlexibleDate{Time: farFuture},
} }
now := time.Now().UTC() 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) require.NoError(t, err)
assert.Equal(t, "upcoming_tasks", resp.Data.KanbanColumn, 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) completedAt := time.Date(2026, 12, 15, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Late", CompletedAt: &completedAt, TaskID: task.ID, Notes: "Late", CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 12, 14, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Early", CompletedAt: &completedAt, TaskID: task.ID, Notes: "Early", CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2014,7 +2014,7 @@ func TestTaskService_FrequencyWithNilOrZeroDays(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Done once", TaskID: task.ID, Notes: "Done once",
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2049,7 +2049,7 @@ func TestTaskService_FrequencyWithNilOrZeroDays(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Done zero", TaskID: task.ID, Notes: "Done zero",
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2076,7 +2076,7 @@ func TestTaskService_VersionConflict(t *testing.T) {
// First update succeeds // First update succeeds
newTitle := "Updated Once" newTitle := "Updated Once"
now := time.Now().UTC() 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, Title: &newTitle,
}, now) }, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2154,7 +2154,7 @@ func TestTaskService_CreateCompletion_OneTime_NextDueDateBecomesNil(t *testing.T
require.NoError(t, err) require.NoError(t, err)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Done", TaskID: task.ID, Notes: "Done",
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2190,7 +2190,7 @@ func TestTaskService_CreateCompletion_OneTime_InProgressBecomesFalse(t *testing.
require.NoError(t, err) require.NoError(t, err)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, TaskID: task.ID,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Record Fields Task")
now := time.Now().UTC() 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", TaskID: task.ID, Notes: "Check all fields",
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2241,7 +2241,7 @@ func TestTaskService_CreateCompletion_OneTime_WithNotesActualCostRating(t *testi
cost := decimal.NewFromFloat(75.50) cost := decimal.NewFromFloat(75.50)
rating := 4 rating := 4
now := time.Now().UTC() now := time.Now().UTC()
resp, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ resp, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, TaskID: task.ID,
Notes: "Detailed notes", Notes: "Detailed notes",
ActualCost: &cost, ActualCost: &cost,
@@ -2292,7 +2292,7 @@ func TestTaskService_CreateCompletion_AlreadyCompleted_SecondCompletionCreated(t
now := time.Now().UTC() now := time.Now().UTC()
// First completion // First completion
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "First", TaskID: task.ID, Notes: "First",
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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") assert.Nil(t, afterFirst.NextDueDate, "NextDueDate should be nil after first completion")
// Second completion on already-completed task // 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", TaskID: task.ID, Notes: "Second",
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) backdated := time.Date(2026, 1, 15, 14, 30, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err := service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err := service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, Notes: "Backdated", CompletedAt: &backdated, TaskID: task.ID, Notes: "Backdated", CompletedAt: &backdated,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) backdated := time.Date(2026, 1, 10, 10, 0, 0, 0, time.UTC)
now := time.Now().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, TaskID: task.ID, Notes: "Backdated recurring", CompletedAt: &backdated,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2425,7 +2425,7 @@ func TestTaskService_CreateCompletion_Recurring_Daily(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
completedAt1 := time.Date(2026, 3, 26, 9, 0, 0, 0, time.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, TaskID: task.ID, CompletedAt: &completedAt1,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2441,7 +2441,7 @@ func TestTaskService_CreateCompletion_Recurring_Daily(t *testing.T) {
// Complete again // Complete again
completedAt2 := time.Date(2026, 3, 27, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, CompletedAt: &completedAt2,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 3, 20, 15, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2527,7 +2527,7 @@ func TestTaskService_CreateCompletion_Recurring_Weekly_CompletedLate_NextDueFrom
// Complete 3 days late // Complete 3 days late
completedAt := time.Date(2026, 3, 23, 10, 0, 0, 0, time.UTC) completedAt := time.Date(2026, 3, 23, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 2, 5, 12, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 1, 5, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 1, 10, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 3, 26, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2859,7 +2859,7 @@ func TestTaskService_CreateCompletion_Recurring_ThreeSequentialCompletions(t *te
// Completion 1: Mar 1 -> NextDueDate = Mar 8 // Completion 1: Mar 1 -> NextDueDate = Mar 8
c1 := time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, CompletedAt: &c1,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2869,7 +2869,7 @@ func TestTaskService_CreateCompletion_Recurring_ThreeSequentialCompletions(t *te
// Completion 2: Mar 8 -> NextDueDate = Mar 15 // Completion 2: Mar 8 -> NextDueDate = Mar 15
c2 := time.Date(2026, 3, 8, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, CompletedAt: &c2,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2879,7 +2879,7 @@ func TestTaskService_CreateCompletion_Recurring_ThreeSequentialCompletions(t *te
// Completion 3: Mar 15 -> NextDueDate = Mar 22 // Completion 3: Mar 15 -> NextDueDate = Mar 22
c3 := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC) 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, TaskID: task.ID, CompletedAt: &c3,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2922,7 +2922,7 @@ func TestTaskService_CreateCompletion_Recurring_InProgressResetForNextCycle(t *t
require.NoError(t, err) require.NoError(t, err)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, TaskID: task.ID,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2960,7 +2960,7 @@ func TestTaskService_CreateCompletion_CompletedFromColumn_Capture(t *testing.T)
require.NoError(t, err) require.NoError(t, err)
now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) 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, TaskID: task.ID,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -2988,7 +2988,7 @@ func TestTaskService_CreateCompletion_CompletedFromColumn_Capture(t *testing.T)
require.NoError(t, err) require.NoError(t, err)
now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC) 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, TaskID: task.ID,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) 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) completedAt := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC)
now := time.Now().UTC() now := time.Now().UTC()
_, err = service.CreateCompletion(&requests.CreateTaskCompletionRequest{ _, err = service.CreateCompletion(context.Background(), &requests.CreateTaskCompletionRequest{
TaskID: task.ID, CompletedAt: &completedAt, TaskID: task.ID, CompletedAt: &completedAt,
}, user.ID, now) }, user.ID, now)
require.NoError(t, err) require.NoError(t, err)
@@ -3091,7 +3091,7 @@ func TestTaskService_QuickComplete_OneTime_ClearsNextDueDate(t *testing.T) {
err := db.Create(task).Error err := db.Create(task).Error
require.NoError(t, err) require.NoError(t, err)
err = service.QuickComplete(task.ID, user.ID) err = service.QuickComplete(context.Background(), task.ID, user.ID)
require.NoError(t, err) require.NoError(t, err)
var reloaded models.Task var reloaded models.Task
@@ -3132,7 +3132,7 @@ func TestTaskService_QuickComplete_Recurring_RecalculatesNextDueDate(t *testing.
err := db.Create(task).Error err := db.Create(task).Error
require.NoError(t, err) require.NoError(t, err)
err = service.QuickComplete(task.ID, user.ID) err = service.QuickComplete(context.Background(), task.ID, user.ID)
require.NoError(t, err) require.NoError(t, err)
var reloaded models.Task var reloaded models.Task
@@ -3154,7 +3154,7 @@ func TestTaskService_QuickComplete_SetsWidgetNotes(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Widget Task") 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) require.NoError(t, err)
var completion models.TaskCompletion 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") 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) 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") residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, owner.ID, "Private Task") 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") testutil.AssertAppError(t, err, http.StatusForbidden, "error.task_access_denied")
} }