Migrate TaskService + ResidenceService to ctx-aware repos
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:
@@ -79,8 +79,8 @@ func (s *TaskService) getSummaryForUser(_ uint) responses.TotalSummary {
|
||||
// === Task CRUD ===
|
||||
|
||||
// GetTask gets a task by ID with access check
|
||||
func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, error) {
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
func (s *TaskService) GetTask(ctx context.Context, taskID, userID uint) (*responses.TaskResponse, error) {
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -89,7 +89,7 @@ func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, err
|
||||
}
|
||||
|
||||
// Check access via residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -164,9 +164,9 @@ func (s *TaskService) GetTasksByResidence(ctx context.Context, residenceID, user
|
||||
|
||||
// CreateTask creates a new task.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
|
||||
func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
func (s *TaskService) CreateTask(ctx context.Context, req *requests.CreateTaskRequest, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
// Check residence access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(req.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -193,12 +193,12 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, n
|
||||
TaskTemplateID: req.TemplateID,
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Create(task); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).Create(task); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Reload with relations
|
||||
task, err = s.taskRepo.FindByID(task.ID)
|
||||
task, err = s.taskRepo.WithContext(ctx).FindByID(task.ID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -216,13 +216,13 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, n
|
||||
//
|
||||
// `now` should be the start of day in the user's timezone for accurate
|
||||
// kanban column categorisation on the returned task list.
|
||||
func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, userID uint, now time.Time) (*responses.BulkCreateTasksResponse, error) {
|
||||
func (s *TaskService) BulkCreateTasks(ctx context.Context, req *requests.BulkCreateTasksRequest, userID uint, now time.Time) (*responses.BulkCreateTasksResponse, error) {
|
||||
if len(req.Tasks) == 0 {
|
||||
return nil, apperrors.BadRequest("error.task_list_empty")
|
||||
}
|
||||
|
||||
// Check residence access once.
|
||||
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(req.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user
|
||||
|
||||
createdIDs := make([]uint, 0, len(req.Tasks))
|
||||
|
||||
err = s.taskRepo.DB().Transaction(func(tx *gorm.DB) error {
|
||||
err = s.taskRepo.WithContext(ctx).DB().Transaction(func(tx *gorm.DB) error {
|
||||
for i := range req.Tasks {
|
||||
entry := req.Tasks[i]
|
||||
// Force the residence ID to the batch-level value so clients
|
||||
@@ -257,7 +257,7 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user
|
||||
ContractorID: entry.ContractorID,
|
||||
TaskTemplateID: entry.TemplateID,
|
||||
}
|
||||
if err := s.taskRepo.CreateTx(tx, task); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).CreateTx(tx, task); err != nil {
|
||||
return fmt.Errorf("create task %d of %d: %w", i+1, len(req.Tasks), err)
|
||||
}
|
||||
createdIDs = append(createdIDs, task.ID)
|
||||
@@ -272,7 +272,7 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user
|
||||
// happen outside the transaction — rows are already committed.
|
||||
created := make([]responses.TaskResponse, 0, len(createdIDs))
|
||||
for _, id := range createdIDs {
|
||||
t, ferr := s.taskRepo.FindByID(id)
|
||||
t, ferr := s.taskRepo.WithContext(ctx).FindByID(id)
|
||||
if ferr != nil {
|
||||
return nil, apperrors.Internal(ferr)
|
||||
}
|
||||
@@ -288,8 +288,8 @@ func (s *TaskService) BulkCreateTasks(req *requests.BulkCreateTasksRequest, user
|
||||
|
||||
// UpdateTask updates a task.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
|
||||
func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
func (s *TaskService) UpdateTask(ctx context.Context, taskID, userID uint, req *requests.UpdateTaskRequest, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -298,7 +298,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -349,7 +349,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
|
||||
task.ContractorID = req.ContractorID
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Update(task); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).Update(task); err != nil {
|
||||
if errors.Is(err, repositories.ErrVersionConflict) {
|
||||
return nil, apperrors.Conflict("error.version_conflict")
|
||||
}
|
||||
@@ -357,7 +357,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
|
||||
}
|
||||
|
||||
// Reload
|
||||
task, err = s.taskRepo.FindByID(task.ID)
|
||||
task, err = s.taskRepo.WithContext(ctx).FindByID(task.ID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -369,8 +369,8 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
|
||||
}
|
||||
|
||||
// DeleteTask deletes a task
|
||||
func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
func (s *TaskService) DeleteTask(ctx context.Context, taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -379,7 +379,7 @@ func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSumm
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -387,7 +387,7 @@ func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSumm
|
||||
return nil, apperrors.Forbidden("error.task_access_denied")
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Delete(taskID); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).Delete(taskID); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
@@ -401,8 +401,8 @@ func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSumm
|
||||
|
||||
// MarkInProgress marks a task as in progress.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
|
||||
func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
func (s *TaskService) MarkInProgress(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -411,7 +411,7 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -419,7 +419,7 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo
|
||||
return nil, apperrors.Forbidden("error.task_access_denied")
|
||||
}
|
||||
|
||||
if err := s.taskRepo.MarkInProgress(taskID, task.Version); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).MarkInProgress(taskID, task.Version); err != nil {
|
||||
if errors.Is(err, repositories.ErrVersionConflict) {
|
||||
return nil, apperrors.Conflict("error.version_conflict")
|
||||
}
|
||||
@@ -427,7 +427,7 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo
|
||||
}
|
||||
|
||||
// Reload
|
||||
task, err = s.taskRepo.FindByID(taskID)
|
||||
task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -440,8 +440,8 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo
|
||||
|
||||
// CancelTask cancels a task.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
|
||||
func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
func (s *TaskService) CancelTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -450,7 +450,7 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -462,7 +462,7 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses
|
||||
return nil, apperrors.BadRequest("error.task_already_cancelled")
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Cancel(taskID, task.Version); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).Cancel(taskID, task.Version); err != nil {
|
||||
if errors.Is(err, repositories.ErrVersionConflict) {
|
||||
return nil, apperrors.Conflict("error.version_conflict")
|
||||
}
|
||||
@@ -470,7 +470,7 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses
|
||||
}
|
||||
|
||||
// Reload
|
||||
task, err = s.taskRepo.FindByID(taskID)
|
||||
task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -483,8 +483,8 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses
|
||||
|
||||
// UncancelTask uncancels a task.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
|
||||
func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
func (s *TaskService) UncancelTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -493,7 +493,7 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -501,7 +501,7 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons
|
||||
return nil, apperrors.Forbidden("error.task_access_denied")
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Uncancel(taskID, task.Version); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).Uncancel(taskID, task.Version); err != nil {
|
||||
if errors.Is(err, repositories.ErrVersionConflict) {
|
||||
return nil, apperrors.Conflict("error.version_conflict")
|
||||
}
|
||||
@@ -509,7 +509,7 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons
|
||||
}
|
||||
|
||||
// Reload
|
||||
task, err = s.taskRepo.FindByID(taskID)
|
||||
task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -522,8 +522,8 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons
|
||||
|
||||
// ArchiveTask archives a task.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
|
||||
func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
func (s *TaskService) ArchiveTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -532,7 +532,7 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -544,7 +544,7 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response
|
||||
return nil, apperrors.BadRequest("error.task_already_archived")
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Archive(taskID, task.Version); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).Archive(taskID, task.Version); err != nil {
|
||||
if errors.Is(err, repositories.ErrVersionConflict) {
|
||||
return nil, apperrors.Conflict("error.version_conflict")
|
||||
}
|
||||
@@ -552,7 +552,7 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response
|
||||
}
|
||||
|
||||
// Reload
|
||||
task, err = s.taskRepo.FindByID(taskID)
|
||||
task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -565,8 +565,8 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response
|
||||
|
||||
// UnarchiveTask unarchives a task.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
|
||||
func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
func (s *TaskService) UnarchiveTask(ctx context.Context, taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -575,7 +575,7 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -583,7 +583,7 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon
|
||||
return nil, apperrors.Forbidden("error.task_access_denied")
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Unarchive(taskID, task.Version); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).Unarchive(taskID, task.Version); err != nil {
|
||||
if errors.Is(err, repositories.ErrVersionConflict) {
|
||||
return nil, apperrors.Conflict("error.version_conflict")
|
||||
}
|
||||
@@ -591,7 +591,7 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon
|
||||
}
|
||||
|
||||
// Reload
|
||||
task, err = s.taskRepo.FindByID(taskID)
|
||||
task, err = s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -606,9 +606,9 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon
|
||||
|
||||
// CreateCompletion creates a task completion.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
|
||||
func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint, now time.Time) (*responses.TaskCompletionWithSummaryResponse, error) {
|
||||
func (s *TaskService) CreateCompletion(ctx context.Context, req *requests.CreateTaskCompletionRequest, userID uint, now time.Time) (*responses.TaskCompletionWithSummaryResponse, error) {
|
||||
// Get the task
|
||||
task, err := s.taskRepo.FindByID(req.TaskID)
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(req.TaskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -617,7 +617,7 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -653,7 +653,7 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
||||
// Note: Frequency is no longer preloaded for performance, so we load it separately if needed
|
||||
var intervalDays *int
|
||||
if task.FrequencyID != nil {
|
||||
frequency, err := s.taskRepo.GetFrequencyByID(*task.FrequencyID)
|
||||
frequency, err := s.taskRepo.WithContext(ctx).GetFrequencyByID(*task.FrequencyID)
|
||||
if err == nil && frequency != nil {
|
||||
if frequency.Name == "Custom" {
|
||||
// Custom frequency - use task's custom_interval_days
|
||||
@@ -681,11 +681,11 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
||||
|
||||
// P1-5 + B-07: Wrap completion creation, task update, and image creation
|
||||
// in a single transaction for atomicity. If any operation fails, all are rolled back.
|
||||
txErr := s.taskRepo.DB().Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.taskRepo.CreateCompletionTx(tx, completion); err != nil {
|
||||
txErr := s.taskRepo.WithContext(ctx).DB().Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.taskRepo.WithContext(ctx).CreateCompletionTx(tx, completion); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.taskRepo.UpdateTx(tx, task); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).UpdateTx(tx, task); err != nil {
|
||||
return err
|
||||
}
|
||||
// B-07: Create images inside the same transaction as completion
|
||||
@@ -712,13 +712,13 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
||||
}
|
||||
|
||||
// Reload completion with user info and images
|
||||
completion, err = s.taskRepo.FindCompletionByID(completion.ID)
|
||||
completion, err = s.taskRepo.WithContext(ctx).FindCompletionByID(completion.ID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Reload task with updated completions (so client can update kanban column)
|
||||
task, err = s.taskRepo.FindByID(req.TaskID)
|
||||
task, err = s.taskRepo.WithContext(ctx).FindByID(req.TaskID)
|
||||
if err != nil {
|
||||
// Non-fatal - still return the completion, just without the task
|
||||
log.Warn().Err(err).Uint("task_id", req.TaskID).Msg("Failed to reload task after completion")
|
||||
@@ -730,7 +730,7 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
||||
}
|
||||
|
||||
// Send notification to residence owner and other users
|
||||
s.sendTaskCompletedNotification(task, completion)
|
||||
s.sendTaskCompletedNotification(ctx, task, completion)
|
||||
|
||||
// Return completion with updated task (includes kanban_column for UI update)
|
||||
resp := responses.NewTaskCompletionWithTaskResponseWithTime(completion, task, 30, now)
|
||||
@@ -744,9 +744,9 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
||||
// LE-01: The entire operation (completion creation + task update) is wrapped in a
|
||||
// transaction for atomicity.
|
||||
// Returns only success/error, no response body.
|
||||
func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
|
||||
func (s *TaskService) QuickComplete(ctx context.Context, taskID uint, userID uint) error {
|
||||
// Get the task
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return apperrors.NotFound("error.task_not_found")
|
||||
@@ -755,7 +755,7 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
@@ -782,7 +782,7 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
|
||||
var quickIntervalDays *int
|
||||
var frequencyName = "unknown"
|
||||
if task.FrequencyID != nil {
|
||||
frequency, err := s.taskRepo.GetFrequencyByID(*task.FrequencyID)
|
||||
frequency, err := s.taskRepo.WithContext(ctx).GetFrequencyByID(*task.FrequencyID)
|
||||
if err == nil && frequency != nil {
|
||||
frequencyName = frequency.Name
|
||||
if frequency.Name == "Custom" {
|
||||
@@ -818,11 +818,11 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
|
||||
}
|
||||
|
||||
// LE-01: Wrap completion creation and task update in a transaction for atomicity
|
||||
txErr := s.taskRepo.DB().Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.taskRepo.CreateCompletionTx(tx, completion); err != nil {
|
||||
txErr := s.taskRepo.WithContext(ctx).DB().Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.taskRepo.WithContext(ctx).CreateCompletionTx(tx, completion); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.taskRepo.UpdateTx(tx, task); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).UpdateTx(tx, task); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -843,23 +843,23 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
|
||||
log.Error().Interface("panic", r).Uint("task_id", task.ID).Msg("Panic in quick-complete notification goroutine")
|
||||
}
|
||||
}()
|
||||
s.sendTaskCompletedNotification(task, completion)
|
||||
s.sendTaskCompletedNotification(ctx, task, completion)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendTaskCompletedNotification sends notifications when a task is completed
|
||||
func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) {
|
||||
func (s *TaskService) sendTaskCompletedNotification(ctx context.Context, task *models.Task, completion *models.TaskCompletion) {
|
||||
// Get all users with access to this residence
|
||||
users, err := s.residenceRepo.GetResidenceUsers(task.ResidenceID)
|
||||
users, err := s.residenceRepo.WithContext(ctx).GetResidenceUsers(task.ResidenceID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to get residence users for notification")
|
||||
return
|
||||
}
|
||||
|
||||
// Get residence name
|
||||
residence, err := s.residenceRepo.FindByIDSimple(task.ResidenceID)
|
||||
residence, err := s.residenceRepo.WithContext(ctx).FindByIDSimple(task.ResidenceID)
|
||||
residenceName := "your property"
|
||||
if err == nil && residence != nil {
|
||||
residenceName = residence.Name
|
||||
@@ -1000,8 +1000,8 @@ func (s *TaskService) getContentTypeFromPath(path string) string {
|
||||
}
|
||||
|
||||
// GetCompletion gets a task completion by ID
|
||||
func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskCompletionResponse, error) {
|
||||
completion, err := s.taskRepo.FindCompletionByID(completionID)
|
||||
func (s *TaskService) GetCompletion(ctx context.Context, completionID, userID uint) (*responses.TaskCompletionResponse, error) {
|
||||
completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.completion_not_found")
|
||||
@@ -1010,7 +1010,7 @@ func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskC
|
||||
}
|
||||
|
||||
// Check access via task's residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(completion.Task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1023,9 +1023,9 @@ func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskC
|
||||
}
|
||||
|
||||
// ListCompletions lists all task completions for a user
|
||||
func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionResponse, error) {
|
||||
func (s *TaskService) ListCompletions(ctx context.Context, userID uint) ([]responses.TaskCompletionResponse, error) {
|
||||
// Get all residence IDs (lightweight - no preloads)
|
||||
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID)
|
||||
residenceIDs, err := s.residenceRepo.WithContext(ctx).FindResidenceIDsByUser(userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1034,7 +1034,7 @@ func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionRe
|
||||
return []responses.TaskCompletionResponse{}, nil
|
||||
}
|
||||
|
||||
completions, err := s.taskRepo.FindCompletionsByUser(userID, residenceIDs)
|
||||
completions, err := s.taskRepo.WithContext(ctx).FindCompletionsByUser(userID, residenceIDs)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1043,8 +1043,8 @@ func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionRe
|
||||
}
|
||||
|
||||
// UpdateCompletion updates an existing task completion
|
||||
func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.UpdateTaskCompletionRequest) (*responses.TaskCompletionResponse, error) {
|
||||
completion, err := s.taskRepo.FindCompletionByID(completionID)
|
||||
func (s *TaskService) UpdateCompletion(ctx context.Context, completionID, userID uint, req *requests.UpdateTaskCompletionRequest) (*responses.TaskCompletionResponse, error) {
|
||||
completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.completion_not_found")
|
||||
@@ -1053,7 +1053,7 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.
|
||||
}
|
||||
|
||||
// Check access via task's residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(completion.Task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1072,7 +1072,7 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.
|
||||
completion.Rating = req.Rating
|
||||
}
|
||||
|
||||
if err := s.taskRepo.UpdateCompletion(completion); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).UpdateCompletion(completion); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
@@ -1082,13 +1082,13 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.
|
||||
CompletionID: completion.ID,
|
||||
ImageURL: imageURL,
|
||||
}
|
||||
if err := s.taskRepo.CreateCompletionImage(image); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).CreateCompletionImage(image); err != nil {
|
||||
log.Error().Err(err).Uint("completion_id", completion.ID).Msg("Failed to create completion image during update")
|
||||
}
|
||||
}
|
||||
|
||||
// Reload to get full associations
|
||||
updated, err := s.taskRepo.FindCompletionByID(completionID)
|
||||
updated, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1102,8 +1102,8 @@ func (s *TaskService) UpdateCompletion(completionID, userID uint, req *requests.
|
||||
// P1-7: After deleting a completion, NextDueDate must be recalculated:
|
||||
// - If no completions remain: restore NextDueDate = DueDate (original schedule)
|
||||
// - If completions remain (recurring): recalculate from latest remaining completion + frequency days
|
||||
func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
|
||||
completion, err := s.taskRepo.FindCompletionByID(completionID)
|
||||
func (s *TaskService) DeleteCompletion(ctx context.Context, completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
|
||||
completion, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.completion_not_found")
|
||||
@@ -1112,7 +1112,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(completion.Task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1122,12 +1122,12 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
|
||||
|
||||
taskID := completion.TaskID
|
||||
|
||||
if err := s.taskRepo.DeleteCompletion(completionID); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).DeleteCompletion(completionID); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Recalculate NextDueDate based on remaining completions
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
// Non-fatal for the delete operation itself, but log the error
|
||||
log.Error().Err(err).Uint("task_id", taskID).Msg("Failed to reload task after completion deletion for NextDueDate recalculation")
|
||||
@@ -1138,7 +1138,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
|
||||
}
|
||||
|
||||
// Get remaining completions for this task
|
||||
remainingCompletions, err := s.taskRepo.FindCompletionsByTask(taskID)
|
||||
remainingCompletions, err := s.taskRepo.WithContext(ctx).FindCompletionsByTask(taskID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Uint("task_id", taskID).Msg("Failed to query remaining completions after deletion")
|
||||
return &responses.DeleteWithSummaryResponse{
|
||||
@@ -1150,7 +1150,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
|
||||
// Determine the task's frequency interval
|
||||
var intervalDays *int
|
||||
if task.FrequencyID != nil {
|
||||
frequency, freqErr := s.taskRepo.GetFrequencyByID(*task.FrequencyID)
|
||||
frequency, freqErr := s.taskRepo.WithContext(ctx).GetFrequencyByID(*task.FrequencyID)
|
||||
if freqErr == nil && frequency != nil {
|
||||
if frequency.Name == "Custom" {
|
||||
intervalDays = task.CustomIntervalDays
|
||||
@@ -1175,7 +1175,7 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
|
||||
task.NextDueDate = nil
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Update(task); err != nil {
|
||||
if err := s.taskRepo.WithContext(ctx).Update(task); err != nil {
|
||||
log.Error().Err(err).Uint("task_id", taskID).Msg("Failed to update task NextDueDate after completion deletion")
|
||||
// The completion was already deleted; return success but log the update failure
|
||||
}
|
||||
@@ -1187,9 +1187,9 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.De
|
||||
}
|
||||
|
||||
// GetCompletionsByTask gets all completions for a specific task
|
||||
func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.TaskCompletionResponse, error) {
|
||||
func (s *TaskService) GetCompletionsByTask(ctx context.Context, taskID, userID uint) ([]responses.TaskCompletionResponse, error) {
|
||||
// Get the task to check access
|
||||
task, err := s.taskRepo.FindByID(taskID)
|
||||
task, err := s.taskRepo.WithContext(ctx).FindByID(taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.task_not_found")
|
||||
@@ -1198,7 +1198,7 @@ func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.Tas
|
||||
}
|
||||
|
||||
// Check access via residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1207,7 +1207,7 @@ func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.Tas
|
||||
}
|
||||
|
||||
// Get completions for the task
|
||||
completions, err := s.taskRepo.FindCompletionsByTask(taskID)
|
||||
completions, err := s.taskRepo.WithContext(ctx).FindCompletionsByTask(taskID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1218,8 +1218,8 @@ func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.Tas
|
||||
// === Lookups ===
|
||||
|
||||
// GetCategories returns all task categories
|
||||
func (s *TaskService) GetCategories() ([]responses.TaskCategoryResponse, error) {
|
||||
categories, err := s.taskRepo.GetAllCategories()
|
||||
func (s *TaskService) GetCategories(ctx context.Context) ([]responses.TaskCategoryResponse, error) {
|
||||
categories, err := s.taskRepo.WithContext(ctx).GetAllCategories()
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1232,8 +1232,8 @@ func (s *TaskService) GetCategories() ([]responses.TaskCategoryResponse, error)
|
||||
}
|
||||
|
||||
// GetPriorities returns all task priorities
|
||||
func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error) {
|
||||
priorities, err := s.taskRepo.GetAllPriorities()
|
||||
func (s *TaskService) GetPriorities(ctx context.Context) ([]responses.TaskPriorityResponse, error) {
|
||||
priorities, err := s.taskRepo.WithContext(ctx).GetAllPriorities()
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -1246,8 +1246,8 @@ func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error)
|
||||
}
|
||||
|
||||
// GetFrequencies returns all task frequencies
|
||||
func (s *TaskService) GetFrequencies() ([]responses.TaskFrequencyResponse, error) {
|
||||
frequencies, err := s.taskRepo.GetAllFrequencies()
|
||||
func (s *TaskService) GetFrequencies(ctx context.Context) ([]responses.TaskFrequencyResponse, error) {
|
||||
frequencies, err := s.taskRepo.WithContext(ctx).GetAllFrequencies()
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user