summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorClaude <agent@claude.ai>2026-03-18 10:04:57 +0000
committerClaude <agent@claude.ai>2026-03-18 10:04:57 +0000
commite85b42d373de55781af9d699b246c0d6a492aec1 (patch)
tree50e40abda62e60144186c1916ddd0f683533c2f4 /internal/api
parente3195a6534bae000a63e884ff647fac95d9d2498 (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.go54
-rw-r--r--internal/api/todoist_test.go51
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"},