Compare commits

..

8 commits

Author SHA1 Message Date
273ebbfa88 docs(changelog): Release v1.0.3 2026-01-18 14:48:56 +01:00
b5e71cc320 bd sync: 2026-01-18 14:42:28 2026-01-18 14:42:28 +01:00
b97877f7fc bd sync: 2026-01-18 14:42:11 2026-01-18 14:42:11 +01:00
d92a1b90c2 Fix Task.tasks field type: rename Children []*Task to ChildIDs []int
The Checkvist API returns the 'tasks' field as an array of child task IDs
(integers), not as full Task objects. This was causing JSON unmarshal errors.

BREAKING CHANGE: Task.Children []*Task renamed to Task.ChildIDs []int
2026-01-18 14:41:42 +01:00
536ccf6ccf bd sync: 2026-01-18 14:41:31 2026-01-18 14:41:31 +01:00
e4dfa7dd72 docs(changelog): Add release entries for v1.0.0, v1.0.1, v1.0.2 2026-01-15 12:21:14 +01:00
f1b47414db bd sync: 2026-01-15 2026-01-15 10:48:52 +01:00
b716d4d0fe Fix Close/Invalidate/Reopen to handle API array response
The Checkvist API returns an array of tasks (containing the modified task
and potentially its subtasks) for close, reopen, and invalidate operations.
The code was incorrectly trying to decode into a single Task struct.

Changes:
- Decode response into []Task instead of Task
- Return first element (the modified task)
- Add defensive error handling for empty arrays
- Update tests to mock array responses

Fixes: checkvist-api-2zr
2026-01-15 10:48:38 +01:00
8 changed files with 130 additions and 23 deletions

View file

@ -3,6 +3,7 @@
{"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:35:43.090941735+01:00","closed_at":"2026-01-14T14:35:43.090941735+01:00","close_reason":"GoDoc examples für alle geforderten Funktionen implementiert","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: \u003c10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:33:38.687033358+01:00","closed_at":"2026-01-14T14:33:38.687033358+01:00","close_reason":"Filter builder mit allen geforderten Methoden implementiert","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-2zr","title":"Fix Close/Invalidate/Reopen to handle array response","description":"## Description\n\nThe `Close()`, `Invalidate()`, and `Reopen()` methods on `TaskService` fail with a JSON decoding error because the Checkvist API returns an array of tasks, not a single task object.\n\n## Error\n\n```go\ntask, err := client.Tasks(945445).Close(ctx, taskID)\nif err != nil {\n panic(err)\n}\n```\n\nResults in:\n```\npanic: decoding response: json: cannot unmarshal array into Go value of type checkvist.Task\n```\n\n## Root Cause\n\nIn `tasks.go` (lines 204-240), all three methods decode the API response into a single `Task`:\n\n```go\nvar task Task\nif err := s.client.doPost(ctx, path, nil, \u0026task); err != nil {\n return nil, err\n}\n```\n\nBut the Checkvist API returns an array containing the modified task and potentially its subtasks.\n\n## Affected Methods\n\n- `TaskService.Close()` (tasks.go:204)\n- `TaskService.Reopen()` (tasks.go:217)\n- `TaskService.Invalidate()` (tasks.go:230)\n\n## Acceptance Criteria\n\n- [ ] All three methods correctly decode the array response\n- [ ] Return the first element (the modified task) as `*Task`\n- [ ] Update tests to use array response fixtures\n- [ ] Add integration test notes in code comments","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:26:22.308158727+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:48:33.012621346+01:00","closed_at":"2026-01-15T10:48:33.012621346+01:00","close_reason":"Closed"}
{"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"}
{"id":"checkvist-api-4pz","title":"Fix DueDate not applied when creating tasks with WithDueDate()","description":"## Problem\n\nWhen creating tasks with `WithDueDate()`, the due date is not set on the created task, even though other parameters (position, priority, tags) work correctly.\n\n## Example Code\n\n```go\ntask, err := client.Tasks(945445).Create(ctx,\n checkvist.NewTask(\"Buy milk\").\n WithPosition(1).\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(2).\n WithTags(\"Shopping\"),\n)\n// Result: task has position=1, priority=2, tags=\"Shopping\", but DueDate is nil\n```\n\n## Analysis\n\nThe JSON being sent to the API looks correct:\n```json\n{\n \"task\": {\n \"content\": \"Buy milk\",\n \"position\": 1,\n \"due_date\": \"^Tomorrow\",\n \"priority\": 2,\n \"tags\": \"Shopping\"\n }\n}\n```\n\nThe task wrapper format is correct (other fields work). The issue is likely with the due_date format:\n- Current format: `\"^Tomorrow\"` (with caret prefix)\n- API might expect: `\"tomorrow\"` (without caret) or different syntax\n\n## Investigation Needed\n\n1. Check Checkvist API documentation for exact `due_date` format\n2. Test with different formats:\n - `\"tomorrow\"` (without ^)\n - `\"Tomorrow\"` (capitalized, without ^)\n - `\"2026-01-16\"` (ISO date)\n3. Check if the `^` prefix is only used in task content, not in API parameters\n\n## Affected Code\n\n- `models.go:178-188` - DueDate constants with `^` prefix\n- `models.go:190-205` - DueAt(), DueString(), DueInDays() functions\n\n## Acceptance Criteria\n\n- [ ] `WithDueDate(DueTomorrow)` creates task with correct due date\n- [ ] `WithDueDate(DueAt(time))` creates task with correct due date\n- [ ] All DueDate constants work correctly","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:11:00.062239196+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:17:38.380280674+01:00","closed_at":"2026-01-15T10:17:38.380280674+01:00","close_reason":"Closed"}
@ -26,6 +27,7 @@
{"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-otm","title":"Fix DueDate parsing: API returns slashes, parser expects dashes","description":"## Problem\n\nWhen fetching tasks via `TaskService.List()`, `DueDate` is always `nil` even for tasks that have a due date set.\n\n## Root Cause\n\nThe Checkvist API returns due dates in slash format (`2026/01/15`), but `parseDueDate()` in `tasks.go:250` only parses the ISO format with dashes:\n\n```go\n// Current implementation - only supports dashes\nif t, err := time.Parse(\"2006-01-02\", task.DueDateRaw); err == nil {\n task.DueDate = \u0026t\n}\n```\n\nAPI response format: `\"due\": \"2026/01/15\"` (slashes)\nExpected by parser: `\"2026-01-15\"` (dashes)\n\n## Solution\n\nExtend `parseDueDate()` to also parse the slash format:\n\n```go\nfunc parseDueDate(task *Task) {\n if task.DueDateRaw == \"\" {\n return\n }\n \n // Try multiple formats\n formats := []string{\n \"2006-01-02\", // ISO format (dashes)\n \"2006/01/02\", // Checkvist API format (slashes)\n }\n \n for _, format := range formats {\n if t, err := time.Parse(format, task.DueDateRaw); err == nil {\n task.DueDate = \u0026t\n return\n }\n }\n}\n```\n\n## Affected Code\n\n- `tasks.go:244-253` - `parseDueDate()` function\n\n## Acceptance Criteria\n\n- [ ] Tasks with due dates have `DueDate` correctly parsed\n- [ ] Both slash format (`2026/01/15`) and dash format (`2026-01-15`) work\n- [ ] Unit test added for slash format parsing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T09:05:02.138578452+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T09:29:14.390484526+01:00","closed_at":"2026-01-15T09:29:14.390484526+01:00","close_reason":"Closed"}
{"id":"checkvist-api-rcj","title":"Fix Task.tasks field: expect []int not []*Task","description":"## Problem\n\nWhen decoding API responses containing the `tasks` field, JSON unmarshaling fails with:\n```\njson: cannot unmarshal number into Go struct field Task.tasks of type checkvist.Task\n```\n\n## Root Cause\n\nIn `models.go:144`, the `Children` field is incorrectly typed:\n```go\nChildren []*Task `json:\"tasks,omitempty\"`\n```\n\nAccording to the Checkvist API documentation, the `tasks` field is:\n\u003e \"tasks [JSON] Javascript array of children task IDs\"\n\nThe API returns an **array of integers** (child task IDs), not an array of full Task objects.\n\n## Solution\n\nChange the field type from `[]*Task` to `[]int` and rename appropriately:\n```go\nChildIDs []int `json:\"tasks,omitempty\"` // IDs of child tasks\n```\n\nThis is a breaking change for users who relied on `Children []*Task`, but the current implementation was incorrect and would fail on real API responses anyway.\n\n## Acceptance Criteria\n\n- [ ] `Task.ChildIDs` correctly unmarshals as `[]int`\n- [ ] Update test fixtures to include `tasks` field with sample child IDs\n- [ ] Tests pass with real API response structure\n- [ ] Update CHANGELOG.md noting the breaking change","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-18T14:32:44.748293162+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-18T14:42:22.954414951+01:00","closed_at":"2026-01-18T14:42:22.954414951+01:00","close_reason":"Fixed Task.tasks field type: renamed Children []*Task to ChildIDs []int"}
{"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]}

View file

@ -3,6 +3,7 @@
{"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:35:43.090941735+01:00","closed_at":"2026-01-14T14:35:43.090941735+01:00","close_reason":"GoDoc examples für alle geforderten Funktionen implementiert","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: <10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:33:38.687033358+01:00","closed_at":"2026-01-14T14:33:38.687033358+01:00","close_reason":"Filter builder mit allen geforderten Methoden implementiert","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-2zr","title":"Fix Close/Invalidate/Reopen to handle array response","description":"## Description\n\nThe `Close()`, `Invalidate()`, and `Reopen()` methods on `TaskService` fail with a JSON decoding error because the Checkvist API returns an array of tasks, not a single task object.\n\n## Error\n\n```go\ntask, err := client.Tasks(945445).Close(ctx, taskID)\nif err != nil {\n panic(err)\n}\n```\n\nResults in:\n```\npanic: decoding response: json: cannot unmarshal array into Go value of type checkvist.Task\n```\n\n## Root Cause\n\nIn `tasks.go` (lines 204-240), all three methods decode the API response into a single `Task`:\n\n```go\nvar task Task\nif err := s.client.doPost(ctx, path, nil, &task); err != nil {\n return nil, err\n}\n```\n\nBut the Checkvist API returns an array containing the modified task and potentially its subtasks.\n\n## Affected Methods\n\n- `TaskService.Close()` (tasks.go:204)\n- `TaskService.Reopen()` (tasks.go:217)\n- `TaskService.Invalidate()` (tasks.go:230)\n\n## Acceptance Criteria\n\n- [ ] All three methods correctly decode the array response\n- [ ] Return the first element (the modified task) as `*Task`\n- [ ] Update tests to use array response fixtures\n- [ ] Add integration test notes in code comments","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:26:22.308158727+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:48:33.012621346+01:00","closed_at":"2026-01-15T10:48:33.012621346+01:00","close_reason":"Closed"}
{"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-47y","title":"Quality & Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target >80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"}
{"id":"checkvist-api-4pz","title":"Fix DueDate not applied when creating tasks with WithDueDate()","description":"## Problem\n\nWhen creating tasks with `WithDueDate()`, the due date is not set on the created task, even though other parameters (position, priority, tags) work correctly.\n\n## Example Code\n\n```go\ntask, err := client.Tasks(945445).Create(ctx,\n checkvist.NewTask(\"Buy milk\").\n WithPosition(1).\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(2).\n WithTags(\"Shopping\"),\n)\n// Result: task has position=1, priority=2, tags=\"Shopping\", but DueDate is nil\n```\n\n## Analysis\n\nThe JSON being sent to the API looks correct:\n```json\n{\n \"task\": {\n \"content\": \"Buy milk\",\n \"position\": 1,\n \"due_date\": \"^Tomorrow\",\n \"priority\": 2,\n \"tags\": \"Shopping\"\n }\n}\n```\n\nThe task wrapper format is correct (other fields work). The issue is likely with the due_date format:\n- Current format: `\"^Tomorrow\"` (with caret prefix)\n- API might expect: `\"tomorrow\"` (without caret) or different syntax\n\n## Investigation Needed\n\n1. Check Checkvist API documentation for exact `due_date` format\n2. Test with different formats:\n - `\"tomorrow\"` (without ^)\n - `\"Tomorrow\"` (capitalized, without ^)\n - `\"2026-01-16\"` (ISO date)\n3. Check if the `^` prefix is only used in task content, not in API parameters\n\n## Affected Code\n\n- `models.go:178-188` - DueDate constants with `^` prefix\n- `models.go:190-205` - DueAt(), DueString(), DueInDays() functions\n\n## Acceptance Criteria\n\n- [ ] `WithDueDate(DueTomorrow)` creates task with correct due date\n- [ ] `WithDueDate(DueAt(time))` creates task with correct due date\n- [ ] All DueDate constants work correctly","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:11:00.062239196+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:17:38.380280674+01:00","closed_at":"2026-01-15T10:17:38.380280674+01:00","close_reason":"Closed"}
@ -26,6 +27,7 @@
{"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-otm","title":"Fix DueDate parsing: API returns slashes, parser expects dashes","description":"## Problem\n\nWhen fetching tasks via `TaskService.List()`, `DueDate` is always `nil` even for tasks that have a due date set.\n\n## Root Cause\n\nThe Checkvist API returns due dates in slash format (`2026/01/15`), but `parseDueDate()` in `tasks.go:250` only parses the ISO format with dashes:\n\n```go\n// Current implementation - only supports dashes\nif t, err := time.Parse(\"2006-01-02\", task.DueDateRaw); err == nil {\n task.DueDate = &t\n}\n```\n\nAPI response format: `\"due\": \"2026/01/15\"` (slashes)\nExpected by parser: `\"2026-01-15\"` (dashes)\n\n## Solution\n\nExtend `parseDueDate()` to also parse the slash format:\n\n```go\nfunc parseDueDate(task *Task) {\n if task.DueDateRaw == \"\" {\n return\n }\n \n // Try multiple formats\n formats := []string{\n \"2006-01-02\", // ISO format (dashes)\n \"2006/01/02\", // Checkvist API format (slashes)\n }\n \n for _, format := range formats {\n if t, err := time.Parse(format, task.DueDateRaw); err == nil {\n task.DueDate = &t\n return\n }\n }\n}\n```\n\n## Affected Code\n\n- `tasks.go:244-253` - `parseDueDate()` function\n\n## Acceptance Criteria\n\n- [ ] Tasks with due dates have `DueDate` correctly parsed\n- [ ] Both slash format (`2026/01/15`) and dash format (`2026-01-15`) work\n- [ ] Unit test added for slash format parsing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T09:05:02.138578452+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T09:29:14.390484526+01:00","closed_at":"2026-01-15T09:29:14.390484526+01:00","close_reason":"Closed"}
{"id":"checkvist-api-rcj","title":"Fix Task.tasks field: expect []int not []*Task","description":"## Problem\n\nWhen decoding API responses containing the `tasks` field, JSON unmarshaling fails with:\n```\njson: cannot unmarshal number into Go struct field Task.tasks of type checkvist.Task\n```\n\n## Root Cause\n\nIn `models.go:144`, the `Children` field is incorrectly typed:\n```go\nChildren []*Task `json:\"tasks,omitempty\"`\n```\n\nAccording to the Checkvist API documentation, the `tasks` field is:\n> \"tasks [JSON] Javascript array of children task IDs\"\n\nThe API returns an **array of integers** (child task IDs), not an array of full Task objects.\n\n## Solution\n\nChange the field type from `[]*Task` to `[]int` and rename appropriately:\n```go\nChildIDs []int `json:\"tasks,omitempty\"` // IDs of child tasks\n```\n\nThis is a breaking change for users who relied on `Children []*Task`, but the current implementation was incorrect and would fail on real API responses anyway.\n\n## Acceptance Criteria\n\n- [ ] `Task.ChildIDs` correctly unmarshals as `[]int`\n- [ ] Update test fixtures to include `tasks` field with sample child IDs\n- [ ] Tests pass with real API response structure\n- [ ] Update CHANGELOG.md noting the breaking change","status":"in_progress","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-18T14:32:44.748293162+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-18T14:34:44.774842767+01:00"}
{"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]}

View file

@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.3] - 2026-01-18
### Changed
- **BREAKING**: `Task.Children []*Task` renamed to `Task.ChildIDs []int` to match API response format (API returns array of child task IDs, not full task objects)
### Fixed
- **Tasks**: Fix `Close()`, `Reopen()`, and `Invalidate()` to handle API array response format
## [1.0.2] - 2026-01-15
### Fixed
- **DueDate**: Remove caret (`^`) prefix from due date values - API expects values like `"Tomorrow"` not `"^Tomorrow"`
### Removed
- **DueDate**: Remove unused `DueASAP` and `DueMonday` constants
## [1.0.1] - 2026-01-15
### Fixed
- **DueDate**: Fix parsing of due dates returned by API - now supports both slash format (`2026/01/15`) and ISO format (`2026-01-15`)
## [1.0.0] - 2026-01-14
### Added
- **Client**: Core API client with functional options pattern

View file

@ -140,8 +140,8 @@ type Task struct {
UpdatedAt APITime `json:"updated_at"`
// CreatedAt is the timestamp when the task was created.
CreatedAt APITime `json:"created_at"`
// Children contains nested child tasks (when fetched with tree structure).
Children []*Task `json:"tasks,omitempty"`
// ChildIDs contains IDs of child tasks (returned by API as array of integers).
ChildIDs []int `json:"tasks,omitempty"`
// Notes contains the comments/notes attached to this task.
Notes []Note `json:"notes,omitempty"`
}

View file

@ -201,42 +201,57 @@ func (s *TaskService) Delete(ctx context.Context, taskID int) error {
}
// Close marks a task as completed.
// The API returns an array containing the modified task and potentially its subtasks.
func (s *TaskService) Close(ctx context.Context, taskID int) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks/%d/close.json", s.checklistID, taskID)
var task Task
if err := s.client.doPost(ctx, path, nil, &task); err != nil {
var tasks []Task
if err := s.client.doPost(ctx, path, nil, &tasks); err != nil {
return nil, err
}
parseDueDate(&task)
return &task, nil
if len(tasks) == 0 {
return nil, fmt.Errorf("close task: unexpected empty response")
}
parseDueDate(&tasks[0])
return &tasks[0], nil
}
// Reopen reopens a closed or invalidated task.
// The API returns an array containing the modified task and potentially its subtasks.
func (s *TaskService) Reopen(ctx context.Context, taskID int) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks/%d/reopen.json", s.checklistID, taskID)
var task Task
if err := s.client.doPost(ctx, path, nil, &task); err != nil {
var tasks []Task
if err := s.client.doPost(ctx, path, nil, &tasks); err != nil {
return nil, err
}
parseDueDate(&task)
return &task, nil
if len(tasks) == 0 {
return nil, fmt.Errorf("reopen task: unexpected empty response")
}
parseDueDate(&tasks[0])
return &tasks[0], nil
}
// Invalidate marks a task as invalidated (strikethrough).
// The API returns an array containing the modified task and potentially its subtasks.
func (s *TaskService) Invalidate(ctx context.Context, taskID int) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks/%d/invalidate.json", s.checklistID, taskID)
var task Task
if err := s.client.doPost(ctx, path, nil, &task); err != nil {
var tasks []Task
if err := s.client.doPost(ctx, path, nil, &tasks); err != nil {
return nil, err
}
parseDueDate(&task)
return &task, nil
if len(tasks) == 0 {
return nil, fmt.Errorf("invalidate task: unexpected empty response")
}
parseDueDate(&tasks[0])
return &tasks[0], nil
}
// parseDueDate attempts to parse the DueDateRaw string into a time.Time.

View file

@ -73,6 +73,60 @@ func TestTasks_Get(t *testing.T) {
}
}
func TestTask_ChildIDs_Unmarshal(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/auth/login.json":
json.NewEncoder(w).Encode(map[string]string{"token": "test-token"})
case "/checklists/1/tasks/101.json":
w.Write(loadFixture(t, "testdata/tasks/single.json"))
case "/checklists/1/tasks.json":
w.Write(loadFixture(t, "testdata/tasks/list.json"))
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}))
defer server.Close()
client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL))
t.Run("single task with child IDs", func(t *testing.T) {
task, err := client.Tasks(1).Get(context.Background(), 101)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedChildIDs := []int{201, 202, 203}
if len(task.ChildIDs) != len(expectedChildIDs) {
t.Fatalf("expected %d child IDs, got %d", len(expectedChildIDs), len(task.ChildIDs))
}
for i, id := range expectedChildIDs {
if task.ChildIDs[i] != id {
t.Errorf("expected ChildIDs[%d] = %d, got %d", i, id, task.ChildIDs[i])
}
}
})
t.Run("list with mixed child IDs", func(t *testing.T) {
tasks, err := client.Tasks(1).List(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// First task has child IDs [201, 202]
if len(tasks[0].ChildIDs) != 2 {
t.Errorf("expected 2 child IDs for first task, got %d", len(tasks[0].ChildIDs))
}
if tasks[0].ChildIDs[0] != 201 || tasks[0].ChildIDs[1] != 202 {
t.Errorf("expected ChildIDs [201, 202], got %v", tasks[0].ChildIDs)
}
// Second task has empty child IDs
if len(tasks[1].ChildIDs) != 0 {
t.Errorf("expected 0 child IDs for second task, got %d", len(tasks[1].ChildIDs))
}
})
}
func TestTasks_Create(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@ -277,10 +331,11 @@ func TestTasks_Close(t *testing.T) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
response := Task{
// API returns an array containing the modified task and potentially subtasks
response := []Task{{
ID: 101,
Status: StatusClosed,
}
}}
json.NewEncoder(w).Encode(response)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
@ -310,10 +365,11 @@ func TestTasks_Reopen(t *testing.T) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
response := Task{
// API returns an array containing the modified task and potentially subtasks
response := []Task{{
ID: 101,
Status: StatusOpen,
}
}}
json.NewEncoder(w).Encode(response)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
@ -343,10 +399,11 @@ func TestTasks_Invalidate(t *testing.T) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
response := Task{
// API returns an array containing the modified task and potentially subtasks
response := []Task{{
ID: 101,
Status: StatusInvalidated,
}
}}
json.NewEncoder(w).Encode(response)
default:
t.Errorf("unexpected path: %s", r.URL.Path)

View file

@ -13,7 +13,8 @@
"comments_count": 0,
"update_line": "",
"updated_at": "2026/01/14 10:00:00 +0000",
"created_at": "2026/01/10 09:00:00 +0000"
"created_at": "2026/01/10 09:00:00 +0000",
"tasks": [201, 202]
},
{
"id": 102,
@ -29,6 +30,7 @@
"comments_count": 3,
"update_line": "",
"updated_at": "2026/01/14 11:00:00 +0000",
"created_at": "2026/01/11 10:00:00 +0000"
"created_at": "2026/01/11 10:00:00 +0000",
"tasks": []
}
]

View file

@ -12,5 +12,6 @@
"comments_count": 0,
"update_line": "",
"updated_at": "2026/01/14 10:00:00 +0000",
"created_at": "2026/01/10 09:00:00 +0000"
"created_at": "2026/01/10 09:00:00 +0000",
"tasks": [201, 202, 203]
}