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
@@ -1,6 +1,7 @@
package services
import (
"context"
"testing"
"time"
@@ -52,7 +53,7 @@ func TestTaskService_CompleteToCancel_OneTimeTask(t *testing.T) {
TaskID: task.ID,
Notes: "Completed",
}
_, err = service.CreateCompletion(completionReq, user.ID, now)
_, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now)
require.NoError(t, err)
// Verify task is completed (NextDueDate=nil)
@@ -66,7 +67,7 @@ func TestTaskService_CompleteToCancel_OneTimeTask(t *testing.T) {
assert.Equal(t, "completed_tasks", column, "Completed one-time task should be in completed column")
// Step 2: Cancel the completed task
cancelResp, err := service.CancelTask(task.ID, user.ID, now)
cancelResp, err := service.CancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, cancelResp.Data.IsCancelled, "Task should be cancelled")
@@ -106,7 +107,7 @@ func TestTaskService_CancelToComplete(t *testing.T) {
now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Step 1: Cancel the task
cancelResp, err := service.CancelTask(task.ID, user.ID, now)
cancelResp, err := service.CancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, cancelResp.Data.IsCancelled)
@@ -115,7 +116,7 @@ func TestTaskService_CancelToComplete(t *testing.T) {
TaskID: task.ID,
Notes: "Completed even though cancelled",
}
completionResp, err := service.CreateCompletion(completionReq, user.ID, now)
completionResp, err := service.CreateCompletion(context.Background(), completionReq, user.ID, now)
require.NoError(t, err)
assert.NotZero(t, completionResp.Data.ID, "Completion should be created")
@@ -160,7 +161,7 @@ func TestTaskService_ArchiveToComplete(t *testing.T) {
now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Step 1: Archive the task
archiveResp, err := service.ArchiveTask(task.ID, user.ID, now)
archiveResp, err := service.ArchiveTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, archiveResp.Data.IsArchived)
@@ -169,7 +170,7 @@ func TestTaskService_ArchiveToComplete(t *testing.T) {
TaskID: task.ID,
Notes: "Completed even though archived",
}
completionResp, err := service.CreateCompletion(completionReq, user.ID, now)
completionResp, err := service.CreateCompletion(context.Background(), completionReq, user.ID, now)
require.NoError(t, err)
assert.NotZero(t, completionResp.Data.ID, "Completion should be created")
@@ -217,11 +218,11 @@ func TestTaskService_CompleteToArchive_OneTimeTask(t *testing.T) {
TaskID: task.ID,
Notes: "Done",
}
_, err = service.CreateCompletion(completionReq, user.ID, now)
_, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now)
require.NoError(t, err)
// Step 2: Archive the completed task
archiveResp, err := service.ArchiveTask(task.ID, user.ID, now)
archiveResp, err := service.ArchiveTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, archiveResp.Data.IsArchived, "Task should be archived")
@@ -266,7 +267,7 @@ func TestTaskService_InProgressToCancelToUncancel(t *testing.T) {
now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Step 1: Mark in progress
inProgressResp, err := service.MarkInProgress(task.ID, user.ID, now)
inProgressResp, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, inProgressResp.Data.InProgress)
@@ -277,7 +278,7 @@ func TestTaskService_InProgressToCancelToUncancel(t *testing.T) {
assert.Equal(t, "in_progress_tasks", column, "Task should be in in_progress column")
// Step 2: Cancel the in-progress task
cancelResp, err := service.CancelTask(task.ID, user.ID, now)
cancelResp, err := service.CancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, cancelResp.Data.IsCancelled)
@@ -292,7 +293,7 @@ func TestTaskService_InProgressToCancelToUncancel(t *testing.T) {
assert.Equal(t, "cancelled_tasks", column, "Cancelled task should be in cancelled column")
// Step 3: Uncancel the task
uncancelResp, err := service.UncancelTask(task.ID, user.ID, now)
uncancelResp, err := service.UncancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.False(t, uncancelResp.Data.IsCancelled)
@@ -336,15 +337,15 @@ func TestTaskService_MultipleCancelUncancelCycles(t *testing.T) {
now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Cycle 1: Cancel -> Uncancel
_, err = service.CancelTask(task.ID, user.ID, now)
_, err = service.CancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
_, err = service.UncancelTask(task.ID, user.ID, now)
_, err = service.UncancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
// Cycle 2: Cancel -> Uncancel
_, err = service.CancelTask(task.ID, user.ID, now)
_, err = service.CancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
uncancelResp, err := service.UncancelTask(task.ID, user.ID, now)
uncancelResp, err := service.UncancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
// Verify final state
@@ -393,7 +394,7 @@ func TestTaskService_CompleteToMarkInProgress_OneTimeTask(t *testing.T) {
TaskID: task.ID,
Notes: "Done",
}
_, err = service.CreateCompletion(completionReq, user.ID, now)
_, err = service.CreateCompletion(context.Background(), completionReq, user.ID, now)
require.NoError(t, err)
// Verify completed state
@@ -402,7 +403,7 @@ func TestTaskService_CompleteToMarkInProgress_OneTimeTask(t *testing.T) {
assert.Nil(t, taskAfterComplete.NextDueDate)
// Step 2: Mark in progress
ipResp, err := service.MarkInProgress(task.ID, user.ID, now)
ipResp, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, ipResp.Data.InProgress, "InProgress should be true")
@@ -452,7 +453,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) {
now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)
// Step 1: Mark in progress
ipResp, err := service.MarkInProgress(task.ID, user.ID, now)
ipResp, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, ipResp.Data.InProgress)
@@ -469,7 +470,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) {
Notes: "Week 1 done",
CompletedAt: &firstCompletedAt,
}
_, err = service.CreateCompletion(completionReq1, user.ID, now)
_, err = service.CreateCompletion(context.Background(), completionReq1, user.ID, now)
require.NoError(t, err)
// Verify: InProgress reset to false, NextDueDate recalculated
@@ -487,7 +488,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) {
assert.NotEqual(t, "completed_tasks", column, "Recurring task should not be in completed column")
// Step 3: Mark in progress again for next cycle
ipResp2, err := service.MarkInProgress(task.ID, user.ID, now)
ipResp2, err := service.MarkInProgress(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, ipResp2.Data.InProgress)
@@ -498,7 +499,7 @@ func TestTaskService_RecurringTaskStateCycle(t *testing.T) {
Notes: "Week 2 done",
CompletedAt: &secondCompletedAt,
}
_, err = service.CreateCompletion(completionReq2, user.ID, now)
_, err = service.CreateCompletion(context.Background(), completionReq2, user.ID, now)
require.NoError(t, err)
// Verify: InProgress reset again, NextDueDate recalculated from second completion
@@ -546,7 +547,7 @@ func TestTaskService_ArchiveToUnarchive(t *testing.T) {
assert.Equal(t, "due_soon_tasks", column, "Task due in 5 days should be due_soon")
// Step 1: Archive
archiveResp, err := service.ArchiveTask(task.ID, user.ID, now)
archiveResp, err := service.ArchiveTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, archiveResp.Data.IsArchived)
@@ -556,7 +557,7 @@ func TestTaskService_ArchiveToUnarchive(t *testing.T) {
assert.Equal(t, "cancelled_tasks", column, "Archived task should be in cancelled column")
// Step 2: Unarchive
unarchiveResp, err := service.UnarchiveTask(task.ID, user.ID, now)
unarchiveResp, err := service.UnarchiveTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.False(t, unarchiveResp.Data.IsArchived)
@@ -738,7 +739,7 @@ func TestTaskService_OptimisticLocking_UpdateWithCorrectVersion(t *testing.T) {
Title: &newTitle,
}
now := time.Now().UTC()
resp, err := service.UpdateTask(task.ID, user.ID, req, now)
resp, err := service.UpdateTask(context.Background(), task.ID, user.ID, req, now)
require.NoError(t, err)
assert.Equal(t, "Updated Title", resp.Data.Title)
@@ -946,7 +947,7 @@ func TestTaskService_OptimisticLocking_ServiceCancelConflictAndRecovery(t *testi
// Reset version and verify service cancel succeeds with correct version
db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 1)
cancelResp, err := service.CancelTask(task.ID, user.ID, now)
cancelResp, err := service.CancelTask(context.Background(), task.ID, user.ID, now)
require.NoError(t, err)
assert.True(t, cancelResp.Data.IsCancelled, "Service cancel should succeed with correct version")
}