diff options
Diffstat (limited to 'backend/handlers')
-rw-r--r-- | backend/handlers/goal_handler.go | 272 | ||||
-rw-r--r-- | backend/handlers/goal_handler_test.go | 471 |
2 files changed, 0 insertions, 743 deletions
diff --git a/backend/handlers/goal_handler.go b/backend/handlers/goal_handler.go deleted file mode 100644 index 09bea74..0000000 --- a/backend/handlers/goal_handler.go +++ /dev/null @@ -1,272 +0,0 @@ -package handlers - -import ( - "log" - "net/http" - "strconv" - "time" - - "finance/backend/internal/database" - "finance/backend/internal/models" - - "github.com/gin-gonic/gin" -) - -// CreateGoalInput defines the structure for creating a new financial goal -type CreateGoalInput struct { - Name string `json:"name" binding:"required"` - TargetAmount int64 `json:"targetAmount" binding:"required"` - CurrentAmount int64 `json:"currentAmount"` - TargetDate string `json:"targetDate"` // YYYY-MM-DD format - Status string `json:"status"` -} - -// UpdateGoalInput defines the structure for updating an existing financial goal -type UpdateGoalInput struct { - Name string `json:"name"` - TargetAmount int64 `json:"targetAmount"` - CurrentAmount int64 `json:"currentAmount"` - TargetDate string `json:"targetDate"` // YYYY-MM-DD format - Status string `json:"status"` -} - -// UpdateGoalProgressInput defines the structure for updating just the progress of a goal -type UpdateGoalProgressInput struct { - CurrentAmount int64 `json:"currentAmount" binding:"required"` -} - -// GoalHandler handles all goal-related operations in the API -type GoalHandler struct { -} - -// NewGoalHandler creates and returns a new GoalHandler instance -func NewGoalHandler() *GoalHandler { - return &GoalHandler{} -} - -// GetGoals retrieves all goals for the authenticated user -// Optionally filtered by status if provided as a query parameter -func (h *GoalHandler) GetGoals(c *gin.Context) { - userID := c.MustGet("userID").(uint) - var goals []models.Goal - - query := database.DB.Where("user_id = ?", userID) - - // Filter by status if provided - status := c.Query("status") - if status != "" { - query = query.Where("status = ?", status) - } - - if err := query.Find(&goals).Error; err != nil { - log.Printf("Error fetching goals for user %d: %v", userID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get goals"}) - return - } - - c.JSON(http.StatusOK, goals) -} - -// GetGoal retrieves a specific goal by ID for the authenticated user -func (h *GoalHandler) GetGoal(c *gin.Context) { - userID := c.MustGet("userID").(uint) - goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - log.Printf("Error parsing goal ID: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"}) - return - } - - var goal models.Goal - if err := database.DB.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil { - log.Printf("Error fetching goal ID %d for user %d: %v", goalID, userID, err) - c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) - return - } - - c.JSON(http.StatusOK, goal) -} - -// CreateGoal creates a new financial goal for the authenticated user -func (h *GoalHandler) CreateGoal(c *gin.Context) { - userID := c.MustGet("userID").(uint) - var input CreateGoalInput - - if err := c.ShouldBindJSON(&input); err != nil { - log.Printf("Error binding JSON for goal creation: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Set default status if not provided - status := "Active" - if input.Status != "" { - status = input.Status - } - - // Parse target date if provided - var targetDate time.Time - if input.TargetDate != "" { - parsedDate, err := time.Parse("2006-01-02", input.TargetDate) - if err != nil { - log.Printf("Error parsing target date for goal creation: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format for targetDate. Use YYYY-MM-DD"}) - return - } - targetDate = parsedDate - } - - goal := models.Goal{ - UserID: userID, - Name: input.Name, - TargetAmount: input.TargetAmount, - CurrentAmount: input.CurrentAmount, - Status: status, - } - - // Only set target date if it was provided - if !targetDate.IsZero() { - goal.TargetDate = targetDate - } - - if err := database.DB.Create(&goal).Error; err != nil { - log.Printf("Error creating goal for user %d: %v", userID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create goal"}) - return - } - - log.Printf("Goal created successfully for user %d: %s (ID: %d)", userID, goal.Name, goal.ID) - c.JSON(http.StatusCreated, goal) -} - -// UpdateGoal updates an existing goal for the authenticated user -func (h *GoalHandler) UpdateGoal(c *gin.Context) { - userID := c.MustGet("userID").(uint) - goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - log.Printf("Error parsing goal ID for update: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"}) - return - } - - var goal models.Goal - if err := database.DB.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil { - log.Printf("Error fetching goal ID %d for user %d in update: %v", goalID, userID, err) - c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) - return - } - - var input UpdateGoalInput - if err := c.ShouldBindJSON(&input); err != nil { - log.Printf("Error binding JSON for goal update: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Update fields that were provided - if input.Name != "" { - goal.Name = input.Name - } - if input.TargetAmount != 0 { - goal.TargetAmount = input.TargetAmount - } - if input.CurrentAmount != 0 { - goal.CurrentAmount = input.CurrentAmount - } - if input.TargetDate != "" { - parsedDate, err := time.Parse("2006-01-02", input.TargetDate) - if err != nil { - log.Printf("Error parsing target date for goal update: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format for targetDate. Use YYYY-MM-DD"}) - return - } - goal.TargetDate = parsedDate - } - if input.Status != "" { - goal.Status = input.Status - } - - // Check if goal has been achieved - if goal.CurrentAmount >= goal.TargetAmount { - goal.Status = "Achieved" - log.Printf("Goal ID %d for user %d automatically marked as Achieved", goalID, userID) - } - - if err := database.DB.Save(&goal).Error; err != nil { - log.Printf("Error saving updated goal ID %d for user %d: %v", goalID, userID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update goal"}) - return - } - - log.Printf("Goal ID %d updated successfully for user %d", goalID, userID) - c.JSON(http.StatusOK, goal) -} - -// UpdateGoalProgress updates just the progress (current amount) of a goal -func (h *GoalHandler) UpdateGoalProgress(c *gin.Context) { - userID := c.MustGet("userID").(uint) - goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - log.Printf("Error parsing goal ID for progress update: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"}) - return - } - - var goal models.Goal - if err := database.DB.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil { - log.Printf("Error fetching goal ID %d for user %d in progress update: %v", goalID, userID, err) - c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) - return - } - - var input UpdateGoalProgressInput - if err := c.ShouldBindJSON(&input); err != nil { - log.Printf("Error binding JSON for goal progress update: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - goal.CurrentAmount = input.CurrentAmount - - // Check if goal has been achieved - if goal.CurrentAmount >= goal.TargetAmount { - goal.Status = "Achieved" - log.Printf("Goal ID %d for user %d automatically marked as Achieved during progress update", goalID, userID) - } - - if err := database.DB.Save(&goal).Error; err != nil { - log.Printf("Error saving progress update for goal ID %d for user %d: %v", goalID, userID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update goal progress"}) - return - } - - log.Printf("Goal ID %d progress updated successfully for user %d: %d/%d", goalID, userID, goal.CurrentAmount, goal.TargetAmount) - c.JSON(http.StatusOK, goal) -} - -// DeleteGoal deletes a goal belonging to the authenticated user -func (h *GoalHandler) DeleteGoal(c *gin.Context) { - userID := c.MustGet("userID").(uint) - goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - log.Printf("Error parsing goal ID for deletion: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"}) - return - } - - var goal models.Goal - if err := database.DB.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil { - log.Printf("Error fetching goal ID %d for user %d for deletion: %v", goalID, userID, err) - c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) - return - } - - if err := database.DB.Delete(&goal).Error; err != nil { - log.Printf("Error deleting goal ID %d for user %d: %v", goalID, userID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete goal"}) - return - } - - log.Printf("Goal ID %d deleted successfully for user %d", goalID, userID) - c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"}) -} diff --git a/backend/handlers/goal_handler_test.go b/backend/handlers/goal_handler_test.go deleted file mode 100644 index d91c000..0000000 --- a/backend/handlers/goal_handler_test.go +++ /dev/null @@ -1,471 +0,0 @@ -package handlers - -import ( - "bytes" - "encoding/json" - "finance/backend/internal/models" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "gorm.io/gorm" -) - -// Create test goals -func createTestGoals() []models.Goal { - return []models.Goal{ - { - Model: gorm.Model{ID: 1}, - UserID: 1, - Name: "Test Goal 1", - TargetAmount: 1000, - CurrentAmount: 500, - Status: "Active", - }, - { - Model: gorm.Model{ID: 2}, - UserID: 1, - Name: "Test Goal 2", - TargetAmount: 2000, - CurrentAmount: 1000, - Status: "Active", - }, - } -} - -// Test data for a single goal -func createTestGoal() models.Goal { - return models.Goal{ - Model: gorm.Model{ID: 1}, - UserID: 1, - Name: "Test Goal", - TargetAmount: 1000, - CurrentAmount: 500, - Status: "Active", - TargetDate: time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC), - } -} - -// setupTestRecorder creates a test context with recorder -func setupTestRecorder() (*httptest.ResponseRecorder, *gin.Context) { - gin.SetMode(gin.TestMode) - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - return w, c -} - -// TestGetGoals tests the GetGoals handler functionality -func TestGetGoals(t *testing.T) { - // Setup test environment - w, c := setupTestRecorder() - c.Set("userID", uint(1)) - req := httptest.NewRequest("GET", "/goals", nil) - c.Request = req - - // Prepare test data - goals := createTestGoals() - - // Directly set response for testing - c.JSON(http.StatusOK, goals) - - // Verify response - assert.Equal(t, http.StatusOK, w.Code) - var responseGoals []models.Goal - err := json.Unmarshal(w.Body.Bytes(), &responseGoals) - assert.NoError(t, err) - assert.Len(t, responseGoals, 2) - assert.Equal(t, "Test Goal 1", responseGoals[0].Name) - assert.Equal(t, "Test Goal 2", responseGoals[1].Name) -} - -// TestGetGoal tests the GetGoal handler -func TestGetGoal(t *testing.T) { - // Test cases - tests := []struct { - name string - userID uint - goalID string - expectedStatus int - expectedError string - }{ - { - name: "Valid Goal ID", - userID: 1, - goalID: "1", - expectedStatus: http.StatusOK, - expectedError: "", - }, - { - name: "Invalid Goal ID", - userID: 1, - goalID: "invalid", - expectedStatus: http.StatusBadRequest, - expectedError: "Invalid goal ID", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup test environment - w, c := setupTestRecorder() - c.Set("userID", tc.userID) - c.AddParam("id", tc.goalID) - req := httptest.NewRequest("GET", "/goals/"+tc.goalID, nil) - c.Request = req - - // Simulate response based on test case - if tc.expectedStatus == http.StatusOK { - goal := createTestGoal() - c.JSON(http.StatusOK, goal) - } else { - c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) - } - - // Verify response - assert.Equal(t, tc.expectedStatus, w.Code) - - if tc.expectedStatus == http.StatusOK { - var goal models.Goal - err := json.Unmarshal(w.Body.Bytes(), &goal) - assert.NoError(t, err) - assert.Equal(t, "Test Goal", goal.Name) - } else { - var errorResponse map[string]string - err := json.Unmarshal(w.Body.Bytes(), &errorResponse) - assert.NoError(t, err) - assert.Equal(t, tc.expectedError, errorResponse["error"]) - } - }) - } -} - -// TestCreateGoal tests the CreateGoal handler -func TestCreateGoal(t *testing.T) { - // Test cases - tests := []struct { - name string - userID uint - input CreateGoalInput - expectedStatus int - expectedError string - }{ - { - name: "Valid Input", - userID: 1, - input: CreateGoalInput{ - Name: "New Goal", - TargetAmount: 5000, - CurrentAmount: 0, - TargetDate: "2024-12-31", - Status: "Active", - }, - expectedStatus: http.StatusCreated, - expectedError: "", - }, - { - name: "Invalid Date Format", - userID: 1, - input: CreateGoalInput{ - Name: "New Goal", - TargetAmount: 5000, - CurrentAmount: 0, - TargetDate: "12/31/2024", // Invalid format - }, - expectedStatus: http.StatusBadRequest, - expectedError: "Invalid date format for targetDate. Use YYYY-MM-DD", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup test environment - w, c := setupTestRecorder() - c.Set("userID", tc.userID) - - // Create request with input data - jsonData, _ := json.Marshal(tc.input) - req := httptest.NewRequest("POST", "/goals", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - c.Request = req - - // Simulate response based on test case - if tc.expectedStatus == http.StatusCreated { - // Create a response goal from the input - goal := models.Goal{ - Model: gorm.Model{ID: 1}, - UserID: tc.userID, - Name: tc.input.Name, - TargetAmount: tc.input.TargetAmount, - CurrentAmount: tc.input.CurrentAmount, - Status: tc.input.Status, - } - if tc.input.TargetDate != "" { - parsedDate, _ := time.Parse("2006-01-02", tc.input.TargetDate) - goal.TargetDate = parsedDate - } - c.JSON(http.StatusCreated, goal) - } else { - c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) - } - - // Verify response - assert.Equal(t, tc.expectedStatus, w.Code) - - if tc.expectedStatus == http.StatusCreated { - var goal models.Goal - err := json.Unmarshal(w.Body.Bytes(), &goal) - assert.NoError(t, err) - assert.Equal(t, tc.input.Name, goal.Name) - } else { - var errorResponse map[string]string - err := json.Unmarshal(w.Body.Bytes(), &errorResponse) - assert.NoError(t, err) - assert.Equal(t, tc.expectedError, errorResponse["error"]) - } - }) - } -} - -// TestUpdateGoal tests the UpdateGoal handler -func TestUpdateGoal(t *testing.T) { - // Test cases - tests := []struct { - name string - userID uint - goalID string - input UpdateGoalInput - expectedStatus int - expectedError string - }{ - { - name: "Valid Update", - userID: 1, - goalID: "1", - input: UpdateGoalInput{ - Name: "Updated Goal", - TargetAmount: 10000, - CurrentAmount: 1000, - TargetDate: "2025-12-31", - Status: "Active", - }, - expectedStatus: http.StatusOK, - expectedError: "", - }, - { - name: "Invalid Goal ID", - userID: 1, - goalID: "invalid", - input: UpdateGoalInput{ - Name: "Updated Goal", - }, - expectedStatus: http.StatusBadRequest, - expectedError: "Invalid goal ID", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup test environment - w, c := setupTestRecorder() - c.Set("userID", tc.userID) - c.AddParam("id", tc.goalID) - - // Create request with input data - jsonData, _ := json.Marshal(tc.input) - req := httptest.NewRequest("PUT", "/goals/"+tc.goalID, bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - c.Request = req - - // Simulate response based on test case - if tc.expectedStatus == http.StatusOK { - // Create a response goal from the input - goal := models.Goal{ - Model: gorm.Model{ID: 1}, - UserID: tc.userID, - Name: tc.input.Name, - TargetAmount: tc.input.TargetAmount, - CurrentAmount: tc.input.CurrentAmount, - Status: tc.input.Status, - } - if tc.input.TargetDate != "" { - parsedDate, _ := time.Parse("2006-01-02", tc.input.TargetDate) - goal.TargetDate = parsedDate - } - c.JSON(http.StatusOK, goal) - } else { - c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) - } - - // Verify response - assert.Equal(t, tc.expectedStatus, w.Code) - }) - } -} - -// TestDeleteGoal tests the DeleteGoal handler -func TestDeleteGoal(t *testing.T) { - // Test cases - tests := []struct { - name string - userID uint - goalID string - expectedStatus int - expectedError string - }{ - { - name: "Successful Delete", - userID: 1, - goalID: "1", - expectedStatus: http.StatusOK, - expectedError: "", - }, - { - name: "Invalid Goal ID", - userID: 1, - goalID: "invalid", - expectedStatus: http.StatusBadRequest, - expectedError: "Invalid goal ID", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup test environment - w, c := setupTestRecorder() - c.Set("userID", tc.userID) - c.AddParam("id", tc.goalID) - req := httptest.NewRequest("DELETE", "/goals/"+tc.goalID, nil) - c.Request = req - - // Simulate response based on test case - if tc.expectedStatus == http.StatusOK { - c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"}) - } else { - c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) - } - - // Verify response - assert.Equal(t, tc.expectedStatus, w.Code) - - // Check response body - var response map[string]string - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err) - - if tc.expectedStatus == http.StatusOK { - assert.Equal(t, "Goal deleted successfully", response["message"]) - } else { - assert.Equal(t, tc.expectedError, response["error"]) - } - }) - } -} - -// TestUpdateGoalProgress tests the UpdateGoalProgress handler -func TestUpdateGoalProgress(t *testing.T) { - // Test cases - tests := []struct { - name string - userID uint - goalID string - input UpdateGoalProgressInput - expectedStatus int - expectedError string - achievesGoal bool - }{ - { - name: "Update Progress", - userID: 1, - goalID: "1", - input: UpdateGoalProgressInput{ - CurrentAmount: 800, - }, - expectedStatus: http.StatusOK, - expectedError: "", - achievesGoal: false, - }, - { - name: "Achieve Goal", - userID: 1, - goalID: "1", - input: UpdateGoalProgressInput{ - CurrentAmount: 1200, // Exceeds target amount of 1000 - }, - expectedStatus: http.StatusOK, - expectedError: "", - achievesGoal: true, - }, - { - name: "Invalid Goal ID", - userID: 1, - goalID: "invalid", - input: UpdateGoalProgressInput{ - CurrentAmount: 800, - }, - expectedStatus: http.StatusBadRequest, - expectedError: "Invalid goal ID", - achievesGoal: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup test environment - w, c := setupTestRecorder() - c.Set("userID", tc.userID) - c.AddParam("id", tc.goalID) - - // Create request with input data - jsonData, _ := json.Marshal(tc.input) - req := httptest.NewRequest("PATCH", "/goals/"+tc.goalID+"/progress", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - c.Request = req - - // Simulate response based on test case - if tc.expectedStatus == http.StatusOK { - // Create a response goal with updated progress - status := "Active" - if tc.achievesGoal { - status = "Achieved" - } - - goal := models.Goal{ - Model: gorm.Model{ID: 1}, - UserID: tc.userID, - Name: "Test Goal", - TargetAmount: 1000, - CurrentAmount: tc.input.CurrentAmount, - Status: status, - TargetDate: time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC), - } - c.JSON(http.StatusOK, goal) - } else { - c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) - } - - // Verify response - assert.Equal(t, tc.expectedStatus, w.Code) - - if tc.expectedStatus == http.StatusOK { - var goal models.Goal - err := json.Unmarshal(w.Body.Bytes(), &goal) - assert.NoError(t, err) - assert.Equal(t, tc.input.CurrentAmount, goal.CurrentAmount) - - if tc.achievesGoal { - assert.Equal(t, "Achieved", goal.Status) - } else { - assert.Equal(t, "Active", goal.Status) - } - } else { - var errorResponse map[string]string - err := json.Unmarshal(w.Body.Bytes(), &errorResponse) - assert.NoError(t, err) - assert.Equal(t, tc.expectedError, errorResponse["error"]) - } - }) - } -} |