Include TotalSummary in CRUD responses to eliminate redundant API calls
Backend changes: - Add WithSummaryResponse wrappers for Task, TaskCompletion, and Residence CRUD - Update services to return summary with all mutations (create, update, delete) - Update handlers to pass through new response types - Add getSummaryForUser helper for fetching summary in CRUD operations - Wire ResidenceService into TaskService for summary access - Add summary field to JoinResidenceResponse This optimization eliminates the need for a separate getSummary() call after every task/residence mutation, reducing network calls from 2 to 1. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -55,11 +55,16 @@ func TestTaskHandler_CreateTask(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Fix leaky faucet", response["title"])
|
||||
assert.Equal(t, "Kitchen faucet is dripping", response["description"])
|
||||
assert.Equal(t, float64(residence.ID), response["residence_id"])
|
||||
assert.Equal(t, false, response["is_cancelled"])
|
||||
assert.Equal(t, false, response["is_archived"])
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
taskData := response["data"].(map[string]interface{})
|
||||
assert.Equal(t, "Fix leaky faucet", taskData["title"])
|
||||
assert.Equal(t, "Kitchen faucet is dripping", taskData["description"])
|
||||
assert.Equal(t, float64(residence.ID), taskData["residence_id"])
|
||||
assert.Equal(t, false, taskData["is_cancelled"])
|
||||
assert.Equal(t, false, taskData["is_archived"])
|
||||
})
|
||||
|
||||
t.Run("task creation with optional fields", func(t *testing.T) {
|
||||
@@ -89,10 +94,15 @@ func TestTaskHandler_CreateTask(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Install new lights", response["title"])
|
||||
assert.NotNil(t, response["category"])
|
||||
assert.NotNil(t, response["priority"])
|
||||
assert.Equal(t, "150.5", response["estimated_cost"]) // Decimal serializes as string
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
taskData := response["data"].(map[string]interface{})
|
||||
assert.Equal(t, "Install new lights", taskData["title"])
|
||||
assert.NotNil(t, taskData["category"])
|
||||
assert.NotNil(t, taskData["priority"])
|
||||
assert.Equal(t, "150.5", taskData["estimated_cost"]) // Decimal serializes as string
|
||||
})
|
||||
|
||||
t.Run("task creation without residence access", func(t *testing.T) {
|
||||
@@ -267,8 +277,13 @@ func TestTaskHandler_UpdateTask(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Updated Title", response["title"])
|
||||
assert.Equal(t, "Updated description", response["description"])
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
taskData := response["data"].(map[string]interface{})
|
||||
assert.Equal(t, "Updated Title", taskData["title"])
|
||||
assert.Equal(t, "Updated description", taskData["description"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -287,8 +302,14 @@ func TestTaskHandler_DeleteTask(t *testing.T) {
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
assert.Contains(t, response["message"], "deleted")
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
assert.Contains(t, response["data"], "deleted")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -311,10 +332,12 @@ func TestTaskHandler_CancelTask(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, response["message"], "cancelled")
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
taskResp := response["task"].(map[string]interface{})
|
||||
assert.Equal(t, true, taskResp["is_cancelled"])
|
||||
taskData := response["data"].(map[string]interface{})
|
||||
assert.Equal(t, true, taskData["is_cancelled"])
|
||||
})
|
||||
|
||||
t.Run("cancel already cancelled task", func(t *testing.T) {
|
||||
@@ -348,8 +371,12 @@ func TestTaskHandler_UncancelTask(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
taskResp := response["task"].(map[string]interface{})
|
||||
assert.Equal(t, false, taskResp["is_cancelled"])
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
taskData := response["data"].(map[string]interface{})
|
||||
assert.Equal(t, false, taskData["is_cancelled"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -372,8 +399,12 @@ func TestTaskHandler_ArchiveTask(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
taskResp := response["task"].(map[string]interface{})
|
||||
assert.Equal(t, true, taskResp["is_archived"])
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
taskData := response["data"].(map[string]interface{})
|
||||
assert.Equal(t, true, taskData["is_archived"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -400,8 +431,12 @@ func TestTaskHandler_UnarchiveTask(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
taskResp := response["task"].(map[string]interface{})
|
||||
assert.Equal(t, false, taskResp["is_archived"])
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
taskData := response["data"].(map[string]interface{})
|
||||
assert.Equal(t, false, taskData["is_archived"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -425,8 +460,10 @@ func TestTaskHandler_MarkInProgress(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, response["message"], "in progress")
|
||||
assert.NotNil(t, response["task"])
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
assert.NotNil(t, response["data"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -456,9 +493,14 @@ func TestTaskHandler_CreateCompletion(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
testutil.AssertJSONFieldExists(t, response, "id")
|
||||
assert.Equal(t, float64(task.ID), response["task_id"])
|
||||
assert.Equal(t, "Completed successfully", response["notes"])
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
completionData := response["data"].(map[string]interface{})
|
||||
testutil.AssertJSONFieldExists(t, completionData, "id")
|
||||
assert.Equal(t, float64(task.ID), completionData["task_id"])
|
||||
assert.Equal(t, "Completed successfully", completionData["notes"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -548,8 +590,14 @@ func TestTaskHandler_DeleteCompletion(t *testing.T) {
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
assert.Contains(t, response["message"], "deleted")
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
assert.Contains(t, response["data"], "deleted")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -645,22 +693,35 @@ func TestTaskHandler_JSONResponses(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Required fields
|
||||
assert.Contains(t, response, "id")
|
||||
assert.Contains(t, response, "residence_id")
|
||||
assert.Contains(t, response, "created_by_id")
|
||||
assert.Contains(t, response, "title")
|
||||
assert.Contains(t, response, "description")
|
||||
assert.Contains(t, response, "is_cancelled")
|
||||
assert.Contains(t, response, "is_archived")
|
||||
assert.Contains(t, response, "created_at")
|
||||
assert.Contains(t, response, "updated_at")
|
||||
// Response should be wrapped in WithSummaryResponse
|
||||
assert.Contains(t, response, "data")
|
||||
assert.Contains(t, response, "summary")
|
||||
|
||||
taskData := response["data"].(map[string]interface{})
|
||||
|
||||
// Required fields in task data
|
||||
assert.Contains(t, taskData, "id")
|
||||
assert.Contains(t, taskData, "residence_id")
|
||||
assert.Contains(t, taskData, "created_by_id")
|
||||
assert.Contains(t, taskData, "title")
|
||||
assert.Contains(t, taskData, "description")
|
||||
assert.Contains(t, taskData, "is_cancelled")
|
||||
assert.Contains(t, taskData, "is_archived")
|
||||
assert.Contains(t, taskData, "created_at")
|
||||
assert.Contains(t, taskData, "updated_at")
|
||||
|
||||
// Type checks
|
||||
assert.IsType(t, float64(0), response["id"])
|
||||
assert.IsType(t, "", response["title"])
|
||||
assert.IsType(t, false, response["is_cancelled"])
|
||||
assert.IsType(t, false, response["is_archived"])
|
||||
assert.IsType(t, float64(0), taskData["id"])
|
||||
assert.IsType(t, "", taskData["title"])
|
||||
assert.IsType(t, false, taskData["is_cancelled"])
|
||||
assert.IsType(t, false, taskData["is_archived"])
|
||||
|
||||
// Summary should have expected fields
|
||||
summary := response["summary"].(map[string]interface{})
|
||||
assert.Contains(t, summary, "total_residences")
|
||||
assert.Contains(t, summary, "total_tasks")
|
||||
assert.Contains(t, summary, "total_pending")
|
||||
assert.Contains(t, summary, "total_overdue")
|
||||
})
|
||||
|
||||
t.Run("list response returns kanban board", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user