From d8856efd30dfcb05b26a5b66b5bb14cc0604e2b1 Mon Sep 17 00:00:00 2001 From: Biswa Kalyan Bhuyan Date: Fri, 25 Apr 2025 02:39:12 +0530 Subject: finance/backend: feat: added v1/goals and handlers/goal_handler for Goal CRUD --- backend/cmd/api/main.go | 12 ++ backend/handlers/goal_handler.go | 243 +++++++++++++++++++++++++++++++++ backend/internal/api/v1/goals/goals.go | 43 ++++++ 3 files changed, 298 insertions(+) create mode 100644 backend/handlers/goal_handler.go create mode 100644 backend/internal/api/v1/goals/goals.go (limited to 'backend') diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index fd5a0ea..e1d0824 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -7,6 +7,7 @@ import ( "finance/backend/internal/api/auth" "finance/backend/internal/api/v1/accounts" + "finance/backend/internal/api/v1/goals" "finance/backend/internal/api/v1/loans" "finance/backend/internal/api/v1/transactions" "finance/backend/internal/config" @@ -121,6 +122,17 @@ func main() { transactionRoutes.DELETE("/:id", transactions.DeleteTransaction()) } + // Goal routes + goalRoutes := protected.Group("/goals") + { + goalRoutes.GET("", goals.GetGoals()) + goalRoutes.GET("/:id", goals.GetGoalByID()) + goalRoutes.POST("", goals.CreateGoal()) + goalRoutes.PUT("/:id", goals.UpdateGoal()) + goalRoutes.DELETE("/:id", goals.DeleteGoal()) + goalRoutes.PATCH("/:id/progress", goals.UpdateGoalProgress()) // Specific endpoint for updating progress + } + // Loan routes loanRoutes := protected.Group("/loans") { diff --git a/backend/handlers/goal_handler.go b/backend/handlers/goal_handler.go new file mode 100644 index 0000000..dd2791c --- /dev/null +++ b/backend/handlers/goal_handler.go @@ -0,0 +1,243 @@ +package handlers + +import ( + "net/http" + "strconv" + "time" + + "finance/backend/internal/database" + "finance/backend/internal/models" + + "github.com/gin-gonic/gin" +) + +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"` +} + +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"` +} + +type UpdateGoalProgressInput struct { + CurrentAmount int64 `json:"currentAmount" binding:"required"` +} + +// GoalHandler handles goal-related operations +type GoalHandler struct { +} + +// NewGoalHandler creates a new goal handler +func NewGoalHandler() *GoalHandler { + return &GoalHandler{} +} + +// GetGoals gets all goals for the current user +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 { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get goals"}) + return + } + + c.JSON(http.StatusOK, goals) +} + +// GetGoal gets a specific goal by ID +func (h *GoalHandler) GetGoal(c *gin.Context) { + userID := c.MustGet("userID").(uint) + goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + 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 { + c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) + return + } + + c.JSON(http.StatusOK, goal) +} + +// CreateGoal creates a new goal for the current user +func (h *GoalHandler) CreateGoal(c *gin.Context) { + userID := c.MustGet("userID").(uint) + var input CreateGoalInput + + if err := c.ShouldBindJSON(&input); err != nil { + 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 { + 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 { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create goal"}) + return + } + + c.JSON(http.StatusCreated, goal) +} + +// UpdateGoal updates an existing goal +func (h *GoalHandler) UpdateGoal(c *gin.Context) { + userID := c.MustGet("userID").(uint) + goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + 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 { + c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) + return + } + + var input UpdateGoalInput + if err := c.ShouldBindJSON(&input); err != nil { + 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 { + 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" + } + + if err := database.DB.Save(&goal).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update goal"}) + return + } + + 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 { + 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 { + c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) + return + } + + var input UpdateGoalProgressInput + if err := c.ShouldBindJSON(&input); err != nil { + 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" + } + + if err := database.DB.Save(&goal).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update goal progress"}) + return + } + + c.JSON(http.StatusOK, goal) +} + +// DeleteGoal deletes a goal +func (h *GoalHandler) DeleteGoal(c *gin.Context) { + userID := c.MustGet("userID").(uint) + goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + 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 { + c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) + return + } + + if err := database.DB.Delete(&goal).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete goal"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"}) +} diff --git a/backend/internal/api/v1/goals/goals.go b/backend/internal/api/v1/goals/goals.go new file mode 100644 index 0000000..1d2cd6f --- /dev/null +++ b/backend/internal/api/v1/goals/goals.go @@ -0,0 +1,43 @@ +package goals + +import ( + "finance/backend/handlers" + + "github.com/gin-gonic/gin" +) + +// GetGoals returns all goals for the authenticated user +func GetGoals() gin.HandlerFunc { + handler := handlers.NewGoalHandler() + return handler.GetGoals +} + +// GetGoalByID returns a specific goal by ID +func GetGoalByID() gin.HandlerFunc { + handler := handlers.NewGoalHandler() + return handler.GetGoal +} + +// CreateGoal creates a new financial goal +func CreateGoal() gin.HandlerFunc { + handler := handlers.NewGoalHandler() + return handler.CreateGoal +} + +// UpdateGoal updates an existing goal +func UpdateGoal() gin.HandlerFunc { + handler := handlers.NewGoalHandler() + return handler.UpdateGoal +} + +// DeleteGoal deletes a goal +func DeleteGoal() gin.HandlerFunc { + handler := handlers.NewGoalHandler() + return handler.DeleteGoal +} + +// UpdateGoalProgress updates just the progress (current amount) of a goal +func UpdateGoalProgress() gin.HandlerFunc { + handler := handlers.NewGoalHandler() + return handler.UpdateGoalProgress +} -- cgit v1.2.3-59-g8ed1b