diff options
| author | Claude <agent@claude.ai> | 2026-03-18 10:04:57 +0000 |
|---|---|---|
| committer | Claude <agent@claude.ai> | 2026-03-18 10:04:57 +0000 |
| commit | e85b42d373de55781af9d699b246c0d6a492aec1 (patch) | |
| tree | 50e40abda62e60144186c1916ddd0f683533c2f4 /internal/api | |
| parent | e3195a6534bae000a63e884ff647fac95d9d2498 (diff) | |
refactor: RF-03/06 extract groupMeals helper, eliminate convertSyncItemToTask wrapper
RF-03: Extract shared groupMeals helper into internal/handlers/meals.go.
Both HandleTabMeals and BuildTimeline now call groupMeals instead of
duplicating the date+mealType grouping algorithm inline. CombinedMeal
gains ID and Meals fields to carry the first-meal ID and original records
needed by BuildTimeline when constructing TimelineItems.
RF-06: Add api.ConvertSyncItemToTask for single-item conversion.
ConvertSyncItemsToTasks now delegates to it, eliminating duplication.
The Handler.convertSyncItemToTask wrapper (which allocated a one-element
slice just to unwrap it) is deleted; its caller uses api.ConvertSyncItemToTask
directly. Covered by TestConvertSyncItemToTask in internal/api/todoist_test.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/todoist.go | 54 | ||||
| -rw-r--r-- | internal/api/todoist_test.go | 51 |
2 files changed, 82 insertions, 23 deletions
diff --git a/internal/api/todoist.go b/internal/api/todoist.go index 2745e3e..be699ce 100644 --- a/internal/api/todoist.go +++ b/internal/api/todoist.go @@ -166,34 +166,42 @@ func (c *TodoistClient) Sync(ctx context.Context, syncToken string) (*TodoistSyn return &syncResp, nil } +// ConvertSyncItemToTask converts a single sync item to a Task model. +// Returns the task and true if the item is active, or a zero Task and false if it should be skipped. +func ConvertSyncItemToTask(item SyncItemResponse, projectMap map[string]string) (models.Task, bool) { + if item.IsCompleted || item.IsDeleted { + return models.Task{}, false + } + + task := models.Task{ + ID: item.ID, + Content: item.Content, + Description: item.Description, + ProjectID: item.ProjectID, + ProjectName: projectMap[item.ProjectID], + Priority: item.Priority, + Completed: false, + Labels: item.Labels, + URL: fmt.Sprintf("https://todoist.com/app/task/%s", item.ID), + } + + if item.AddedAt != "" { + if createdAt, err := time.Parse(time.RFC3339, item.AddedAt); err == nil { + task.CreatedAt = createdAt + } + } + + task.DueDate = parseDueDate(item.Due) + return task, true +} + // ConvertSyncItemsToTasks converts sync API items to Task models func ConvertSyncItemsToTasks(items []SyncItemResponse, projectMap map[string]string) []models.Task { tasks := make([]models.Task, 0, len(items)) for _, item := range items { - if item.IsCompleted || item.IsDeleted { - continue - } - - task := models.Task{ - ID: item.ID, - Content: item.Content, - Description: item.Description, - ProjectID: item.ProjectID, - ProjectName: projectMap[item.ProjectID], - Priority: item.Priority, - Completed: false, - Labels: item.Labels, - URL: fmt.Sprintf("https://todoist.com/app/task/%s", item.ID), + if task, ok := ConvertSyncItemToTask(item, projectMap); ok { + tasks = append(tasks, task) } - - if item.AddedAt != "" { - if createdAt, err := time.Parse(time.RFC3339, item.AddedAt); err == nil { - task.CreatedAt = createdAt - } - } - - task.DueDate = parseDueDate(item.Due) - tasks = append(tasks, task) } return tasks } diff --git a/internal/api/todoist_test.go b/internal/api/todoist_test.go index 159be61..2204469 100644 --- a/internal/api/todoist_test.go +++ b/internal/api/todoist_test.go @@ -421,6 +421,57 @@ func TestConvertSyncItemsToTasks(t *testing.T) { } } +func TestConvertSyncItemToTask(t *testing.T) { + projects := map[string]string{"proj-1": "Project 1"} + + t.Run("active item returns task and true", func(t *testing.T) { + item := SyncItemResponse{ + ID: "item-1", + Content: "Active Task", + Description: "desc", + ProjectID: "proj-1", + Priority: 2, + Labels: []string{"work"}, + AddedAt: "2026-01-01T00:00:00Z", + } + task, ok := ConvertSyncItemToTask(item, projects) + if !ok { + t.Fatal("expected ok=true for active item") + } + if task.ID != "item-1" { + t.Errorf("expected ID 'item-1', got '%s'", task.ID) + } + if task.Content != "Active Task" { + t.Errorf("expected Content 'Active Task', got '%s'", task.Content) + } + if task.ProjectName != "Project 1" { + t.Errorf("expected ProjectName 'Project 1', got '%s'", task.ProjectName) + } + if task.Completed { + t.Error("expected Completed=false") + } + if task.URL != "https://todoist.com/app/task/item-1" { + t.Errorf("unexpected URL: %s", task.URL) + } + }) + + t.Run("completed item returns false", func(t *testing.T) { + item := SyncItemResponse{ID: "item-2", Content: "Done", ProjectID: "proj-1", IsCompleted: true} + _, ok := ConvertSyncItemToTask(item, projects) + if ok { + t.Error("expected ok=false for completed item") + } + }) + + t.Run("deleted item returns false", func(t *testing.T) { + item := SyncItemResponse{ID: "item-3", Content: "Gone", ProjectID: "proj-1", IsDeleted: true} + _, ok := ConvertSyncItemToTask(item, projects) + if ok { + t.Error("expected ok=false for deleted item") + } + }) +} + func TestBuildProjectMapFromSync(t *testing.T) { projects := []SyncProjectResponse{ {ID: "proj-1", Name: "Project 1"}, |
