diff options
Diffstat (limited to 'backend/internal/api/handlers')
-rw-r--r-- | backend/internal/api/handlers/account_handler.go | 162 | ||||
-rw-r--r-- | backend/internal/api/handlers/auth_handlers.go | 38 | ||||
-rw-r--r-- | backend/internal/api/handlers/goal_handler.go | 272 | ||||
-rw-r--r-- | backend/internal/api/handlers/goal_handler_test.go | 471 | ||||
-rw-r--r-- | backend/internal/api/handlers/loan_handler.go | 239 | ||||
-rw-r--r-- | backend/internal/api/handlers/transaction_handler.go | 387 | ||||
-rw-r--r-- | backend/internal/api/handlers/user_handler.go | 69 |
7 files changed, 1638 insertions, 0 deletions
diff --git a/backend/internal/api/handlers/account_handler.go b/backend/internal/api/handlers/account_handler.go new file mode 100644 index 0000000..5a92ef6 --- /dev/null +++ b/backend/internal/api/handlers/account_handler.go @@ -0,0 +1,162 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + + "finance/backend/internal/database" + "finance/backend/internal/models" + + "github.com/gin-gonic/gin" +) + +// AccountHandler handles account-related operations +type AccountHandler struct { +} + +// NewAccountHandler creates and returns a new AccountHandler instance +func NewAccountHandler() *AccountHandler { + return &AccountHandler{} +} + +// GetAccounts retrieves all accounts for the authenticated user +func (h *AccountHandler) GetAccounts(c *gin.Context) { + userID := c.MustGet("userID").(uint) + var accounts []models.Account + + // Fetch accounts for the current user + if err := database.DB.Where("user_id = ?", userID).Find(&accounts).Error; err != nil { + log.Printf("Error fetching accounts for user %d: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get accounts"}) + return + } + + c.JSON(http.StatusOK, accounts) +} + +// GetAccountByID retrieves a specific account by ID for the authenticated user +func (h *AccountHandler) GetAccountByID(c *gin.Context) { + userID := c.MustGet("userID").(uint) + accountID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid account ID"}) + return + } + + var account models.Account + if err := database.DB.Where("id = ? AND user_id = ?", accountID, userID).First(&account).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Account not found"}) + return + } + + c.JSON(http.StatusOK, account) +} + +// CreateAccount creates a new financial account for the authenticated user +func (h *AccountHandler) CreateAccount(c *gin.Context) { + userID := c.MustGet("userID").(uint) + + // Define input structure + type CreateAccountInput struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Balance int64 `json:"balance"` + } + + var input CreateAccountInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Create new account + account := models.Account{ + UserID: userID, + Name: input.Name, + Type: input.Type, + Balance: input.Balance, + } + + if err := database.DB.Create(&account).Error; err != nil { + log.Printf("Error creating account for user %d: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create account"}) + return + } + + c.JSON(http.StatusCreated, account) +} + +// UpdateAccount updates an existing account for the authenticated user +func (h *AccountHandler) UpdateAccount(c *gin.Context) { + userID := c.MustGet("userID").(uint) + accountID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid account ID"}) + return + } + + // Find account + var account models.Account + if err := database.DB.Where("id = ? AND user_id = ?", accountID, userID).First(&account).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Account not found"}) + return + } + + // Define update structure + type UpdateAccountInput struct { + Name string `json:"name"` + Type string `json:"type"` + Balance int64 `json:"balance"` + } + + var input UpdateAccountInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update fields if provided + if input.Name != "" { + account.Name = input.Name + } + if input.Type != "" { + account.Type = input.Type + } + // For balance updates, we'll accept zero values, so we update unconditionally + account.Balance = input.Balance + + if err := database.DB.Save(&account).Error; err != nil { + log.Printf("Error updating account ID %d for user %d: %v", accountID, userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update account"}) + return + } + + c.JSON(http.StatusOK, account) +} + +// DeleteAccount deletes an account belonging to the authenticated user +func (h *AccountHandler) DeleteAccount(c *gin.Context) { + userID := c.MustGet("userID").(uint) + accountID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid account ID"}) + return + } + + // Check if account exists and belongs to user + var account models.Account + if err := database.DB.Where("id = ? AND user_id = ?", accountID, userID).First(&account).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Account not found"}) + return + } + + // Delete account + if err := database.DB.Delete(&account).Error; err != nil { + log.Printf("Error deleting account ID %d for user %d: %v", accountID, userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete account"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Account deleted successfully"}) +} diff --git a/backend/internal/api/handlers/auth_handlers.go b/backend/internal/api/handlers/auth_handlers.go new file mode 100644 index 0000000..18d8a1a --- /dev/null +++ b/backend/internal/api/handlers/auth_handlers.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "finance/backend/internal/api/auth" + "finance/backend/internal/config" + + "github.com/gin-gonic/gin" +) + +// AuthHandler handles authentication-related operations +type AuthHandler struct { + config *config.Config +} + +// NewAuthHandler creates and returns a new AuthHandler instance +func NewAuthHandler(cfg *config.Config) *AuthHandler { + return &AuthHandler{ + config: cfg, + } +} + +// Register handles user registration +func (h *AuthHandler) Register(c *gin.Context) { + // Delegate to the existing auth.Signup handler + auth.Signup(h.config)(c) +} + +// Login handles user authentication +func (h *AuthHandler) Login(c *gin.Context) { + // Delegate to the existing auth.Login handler + auth.Login(h.config)(c) +} + +// JWTAuth middleware validates JWT tokens +func (h *AuthHandler) JWTAuth(c *gin.Context) { + // Delegate to the existing auth.AuthMiddleware + auth.AuthMiddleware(h.config)(c) +} diff --git a/backend/internal/api/handlers/goal_handler.go b/backend/internal/api/handlers/goal_handler.go new file mode 100644 index 0000000..09bea74 --- /dev/null +++ b/backend/internal/api/handlers/goal_handler.go @@ -0,0 +1,272 @@ +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/internal/api/handlers/goal_handler_test.go b/backend/internal/api/handlers/goal_handler_test.go new file mode 100644 index 0000000..d91c000 --- /dev/null +++ b/backend/internal/api/handlers/goal_handler_test.go @@ -0,0 +1,471 @@ +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"]) + } + }) + } +} diff --git a/backend/internal/api/handlers/loan_handler.go b/backend/internal/api/handlers/loan_handler.go new file mode 100644 index 0000000..3edb559 --- /dev/null +++ b/backend/internal/api/handlers/loan_handler.go @@ -0,0 +1,239 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + "time" + + "finance/backend/internal/database" + "finance/backend/internal/models" + + "github.com/gin-gonic/gin" +) + +// LoanHandler handles loan-related operations +type LoanHandler struct { +} + +// NewLoanHandler creates and returns a new LoanHandler instance +func NewLoanHandler() *LoanHandler { + return &LoanHandler{} +} + +// GetLoans retrieves all loans for the authenticated user +func (h *LoanHandler) GetLoans(c *gin.Context) { + userID := c.MustGet("userID").(uint) + var loans []models.Loan + + if err := database.DB.Where("user_id = ?", userID).Find(&loans).Error; err != nil { + log.Printf("Error fetching loans for user %d: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get loans"}) + return + } + + c.JSON(http.StatusOK, loans) +} + +// GetLoanByID retrieves a specific loan by ID for the authenticated user +func (h *LoanHandler) GetLoanByID(c *gin.Context) { + userID := c.MustGet("userID").(uint) + loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID"}) + return + } + + var loan models.Loan + if err := database.DB.Where("id = ? AND user_id = ?", loanID, userID).First(&loan).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) + return + } + + c.JSON(http.StatusOK, loan) +} + +// CreateLoan creates a new loan for the authenticated user +func (h *LoanHandler) CreateLoan(c *gin.Context) { + userID := c.MustGet("userID").(uint) + + // Define input structure + type CreateLoanInput struct { + Name string `json:"name" binding:"required"` + OriginalAmount int64 `json:"originalAmount" binding:"required"` + CurrentBalance int64 `json:"currentBalance" binding:"required"` + InterestRate float64 `json:"interestRate"` + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + AccountID *uint `json:"accountId"` + } + + var input CreateLoanInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Parse dates if provided + var startDate, endDate time.Time + var err error + + if input.StartDate != "" { + startDate, err = time.Parse("2006-01-02", input.StartDate) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start date format. Use YYYY-MM-DD"}) + return + } + } + + if input.EndDate != "" { + endDate, err = time.Parse("2006-01-02", input.EndDate) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end date format. Use YYYY-MM-DD"}) + return + } + } + + // Validate account if provided + if input.AccountID != nil { + var account models.Account + if err := database.DB.Where("id = ? AND user_id = ?", *input.AccountID, userID).First(&account).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Associated account not found or doesn't belong to user"}) + return + } + } + + // Create loan + loan := models.Loan{ + UserID: userID, + AccountID: input.AccountID, + Name: input.Name, + OriginalAmount: input.OriginalAmount, + CurrentBalance: input.CurrentBalance, + InterestRate: input.InterestRate, + } + + // Only set dates if they were provided + if !startDate.IsZero() { + loan.StartDate = startDate + } + if !endDate.IsZero() { + loan.EndDate = endDate + } + + if err := database.DB.Create(&loan).Error; err != nil { + log.Printf("Error creating loan for user %d: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create loan"}) + return + } + + c.JSON(http.StatusCreated, loan) +} + +// UpdateLoan updates an existing loan for the authenticated user +func (h *LoanHandler) UpdateLoan(c *gin.Context) { + userID := c.MustGet("userID").(uint) + loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID"}) + return + } + + // Find loan + var loan models.Loan + if err := database.DB.Where("id = ? AND user_id = ?", loanID, userID).First(&loan).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) + return + } + + // Define update structure + type UpdateLoanInput struct { + Name string `json:"name"` + OriginalAmount int64 `json:"originalAmount"` + CurrentBalance int64 `json:"currentBalance"` + InterestRate float64 `json:"interestRate"` + StartDate string `json:"startDate"` + EndDate string `json:"endDate"` + AccountID *uint `json:"accountId"` + } + + var input UpdateLoanInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update fields if provided + if input.Name != "" { + loan.Name = input.Name + } + if input.OriginalAmount != 0 { + loan.OriginalAmount = input.OriginalAmount + } + if input.CurrentBalance != 0 { + loan.CurrentBalance = input.CurrentBalance + } + if input.InterestRate != 0 { + loan.InterestRate = input.InterestRate + } + if input.StartDate != "" { + startDate, err := time.Parse("2006-01-02", input.StartDate) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start date format. Use YYYY-MM-DD"}) + return + } + loan.StartDate = startDate + } + if input.EndDate != "" { + endDate, err := time.Parse("2006-01-02", input.EndDate) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end date format. Use YYYY-MM-DD"}) + return + } + loan.EndDate = endDate + } + if input.AccountID != nil { + // Validate account if provided + var account models.Account + if err := database.DB.Where("id = ? AND user_id = ?", *input.AccountID, userID).First(&account).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Associated account not found or doesn't belong to user"}) + return + } + loan.AccountID = input.AccountID + } + + // Save changes + if err := database.DB.Save(&loan).Error; err != nil { + log.Printf("Error updating loan ID %d for user %d: %v", loanID, userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update loan"}) + return + } + + c.JSON(http.StatusOK, loan) +} + +// DeleteLoan deletes a loan belonging to the authenticated user +func (h *LoanHandler) DeleteLoan(c *gin.Context) { + userID := c.MustGet("userID").(uint) + loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID"}) + return + } + + // Check if loan exists and belongs to user + var loan models.Loan + if err := database.DB.Where("id = ? AND user_id = ?", loanID, userID).First(&loan).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) + return + } + + // Delete loan + if err := database.DB.Delete(&loan).Error; err != nil { + log.Printf("Error deleting loan ID %d for user %d: %v", loanID, userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete loan"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Loan deleted successfully"}) +} diff --git a/backend/internal/api/handlers/transaction_handler.go b/backend/internal/api/handlers/transaction_handler.go new file mode 100644 index 0000000..542d01a --- /dev/null +++ b/backend/internal/api/handlers/transaction_handler.go @@ -0,0 +1,387 @@ +package handlers + +import ( + "log" + "net/http" + "strconv" + "time" + + "finance/backend/internal/database" + "finance/backend/internal/models" + + "github.com/gin-gonic/gin" +) + +// TransactionHandler handles transaction-related operations +type TransactionHandler struct { +} + +// NewTransactionHandler creates and returns a new TransactionHandler instance +func NewTransactionHandler() *TransactionHandler { + return &TransactionHandler{} +} + +// GetTransactions retrieves all transactions for the authenticated user +// Can be filtered by account, type, and date range +func (h *TransactionHandler) GetTransactions(c *gin.Context) { + userID := c.MustGet("userID").(uint) + var transactions []models.Transaction + + query := database.DB.Where("user_id = ?", userID) + + // Filter by account if provided + if accountIDStr := c.Query("account_id"); accountIDStr != "" { + accountID, err := strconv.ParseUint(accountIDStr, 10, 32) + if err == nil { + query = query.Where("account_id = ?", accountID) + } + } + + // Filter by type if provided (income or expense) + if txType := c.Query("type"); txType != "" { + query = query.Where("type = ?", txType) + } + + // Filter by category if provided + if category := c.Query("category"); category != "" { + query = query.Where("category = ?", category) + } + + // Filter by date range if provided + if fromDate := c.Query("from_date"); fromDate != "" { + date, err := time.Parse("2006-01-02", fromDate) + if err == nil { + query = query.Where("date >= ?", date) + } + } + + if toDate := c.Query("to_date"); toDate != "" { + date, err := time.Parse("2006-01-02", toDate) + if err == nil { + // Add 1 day to include the end date + date = date.Add(24 * time.Hour) + query = query.Where("date < ?", date) + } + } + + // Order by date (most recent first) + query = query.Order("date DESC") + + // Execute the query + if err := query.Find(&transactions).Error; err != nil { + log.Printf("Error fetching transactions for user %d: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get transactions"}) + return + } + + c.JSON(http.StatusOK, transactions) +} + +// GetTransactionByID retrieves a specific transaction by ID for the authenticated user +func (h *TransactionHandler) GetTransactionByID(c *gin.Context) { + userID := c.MustGet("userID").(uint) + txID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid transaction ID"}) + return + } + + var transaction models.Transaction + if err := database.DB.Where("id = ? AND user_id = ?", txID, userID).First(&transaction).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"}) + return + } + + c.JSON(http.StatusOK, transaction) +} + +// CreateTransaction creates a new transaction for the authenticated user +func (h *TransactionHandler) CreateTransaction(c *gin.Context) { + userID := c.MustGet("userID").(uint) + + // Define input structure + type CreateTransactionInput struct { + AccountID *uint `json:"accountId"` + Description string `json:"description" binding:"required"` + Amount int64 `json:"amount" binding:"required"` + Type string `json:"type" binding:"required"` // "Income" or "Expense" + Date string `json:"date" binding:"required"` // "YYYY-MM-DD" format + Category string `json:"category"` + } + + var input CreateTransactionInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validate transaction type + if input.Type != "Income" && input.Type != "Expense" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Type must be either 'Income' or 'Expense'"}) + return + } + + // Parse the date + date, err := time.Parse("2006-01-02", input.Date) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format. Use YYYY-MM-DD"}) + return + } + + // Create transaction object + transaction := models.Transaction{ + UserID: userID, + AccountID: input.AccountID, + Description: input.Description, + Amount: input.Amount, + Type: input.Type, + Date: date, + Category: input.Category, + } + + // Begin a transaction to update both the transaction and account balance + tx := database.DB.Begin() + + // Create the transaction + if err := tx.Create(&transaction).Error; err != nil { + tx.Rollback() + log.Printf("Error creating transaction for user %d: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create transaction"}) + return + } + + // Update account balance if account ID is provided + if input.AccountID != nil { + var account models.Account + if err := tx.First(&account, *input.AccountID).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusBadRequest, gin.H{"error": "Account not found"}) + return + } + + // Make sure the account belongs to the user + if account.UserID != userID { + tx.Rollback() + c.JSON(http.StatusForbidden, gin.H{"error": "Account does not belong to user"}) + return + } + + // Update the account balance + if input.Type == "Income" { + account.Balance += input.Amount + } else { // Expense + account.Balance -= input.Amount + } + + if err := tx.Save(&account).Error; err != nil { + tx.Rollback() + log.Printf("Error updating account balance for user %d: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update account balance"}) + return + } + } + + // Commit the transaction + if err := tx.Commit().Error; err != nil { + log.Printf("Error committing transaction for user %d: %v", userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save changes"}) + return + } + + c.JSON(http.StatusCreated, transaction) +} + +// UpdateTransaction updates an existing transaction for the authenticated user +func (h *TransactionHandler) UpdateTransaction(c *gin.Context) { + userID := c.MustGet("userID").(uint) + txID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid transaction ID"}) + return + } + + // Get the original transaction + var transaction models.Transaction + if err := database.DB.Where("id = ? AND user_id = ?", txID, userID).First(&transaction).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"}) + return + } + + // Define input structure + type UpdateTransactionInput struct { + AccountID *uint `json:"accountId"` + Description string `json:"description"` + Amount int64 `json:"amount"` + Type string `json:"type"` // "Income" or "Expense" + Date string `json:"date"` // "YYYY-MM-DD" format + Category string `json:"category"` + } + + var input UpdateTransactionInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Begin a database transaction + tx := database.DB.Begin() + + // Save original values for account balance calculation + originalAmount := transaction.Amount + originalType := transaction.Type + originalAccountID := transaction.AccountID + + // Update fields if provided + if input.Description != "" { + transaction.Description = input.Description + } + if input.Amount != 0 { + transaction.Amount = input.Amount + } + if input.Type != "" { + if input.Type != "Income" && input.Type != "Expense" { + tx.Rollback() + c.JSON(http.StatusBadRequest, gin.H{"error": "Type must be either 'Income' or 'Expense'"}) + return + } + transaction.Type = input.Type + } + if input.Date != "" { + date, err := time.Parse("2006-01-02", input.Date) + if err != nil { + tx.Rollback() + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format. Use YYYY-MM-DD"}) + return + } + transaction.Date = date + } + if input.Category != "" { + transaction.Category = input.Category + } + if input.AccountID != nil { + transaction.AccountID = input.AccountID + } + + // Update the transaction + if err := tx.Save(&transaction).Error; err != nil { + tx.Rollback() + log.Printf("Error updating transaction ID %d for user %d: %v", txID, userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update transaction"}) + return + } + + // Handle account balance updates if needed + // 1. Revert the original transaction effect on the original account + if originalAccountID != nil { + var originalAccount models.Account + if err := tx.First(&originalAccount, *originalAccountID).Error; err == nil { + if originalType == "Income" { + originalAccount.Balance -= originalAmount + } else { // Expense + originalAccount.Balance += originalAmount + } + if err := tx.Save(&originalAccount).Error; err != nil { + tx.Rollback() + log.Printf("Error updating original account balance for transaction %d: %v", txID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update account balance"}) + return + } + } + } + + // 2. Apply the new transaction effect on the new account + if transaction.AccountID != nil { + var newAccount models.Account + if err := tx.First(&newAccount, *transaction.AccountID).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusBadRequest, gin.H{"error": "Account not found"}) + return + } + + // Make sure the account belongs to the user + if newAccount.UserID != userID { + tx.Rollback() + c.JSON(http.StatusForbidden, gin.H{"error": "Account does not belong to user"}) + return + } + + // Update the account balance + if transaction.Type == "Income" { + newAccount.Balance += transaction.Amount + } else { // Expense + newAccount.Balance -= transaction.Amount + } + + if err := tx.Save(&newAccount).Error; err != nil { + tx.Rollback() + log.Printf("Error updating new account balance for transaction %d: %v", txID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update account balance"}) + return + } + } + + // Commit the transaction + if err := tx.Commit().Error; err != nil { + log.Printf("Error committing updates for transaction %d: %v", txID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save changes"}) + return + } + + c.JSON(http.StatusOK, transaction) +} + +// DeleteTransaction deletes a transaction belonging to the authenticated user +func (h *TransactionHandler) DeleteTransaction(c *gin.Context) { + userID := c.MustGet("userID").(uint) + txID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid transaction ID"}) + return + } + + // Get the transaction + var transaction models.Transaction + if err := database.DB.Where("id = ? AND user_id = ?", txID, userID).First(&transaction).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"}) + return + } + + // Begin a database transaction + tx := database.DB.Begin() + + // Remove the transaction's effect from the account balance if applicable + if transaction.AccountID != nil { + var account models.Account + if err := tx.First(&account, *transaction.AccountID).Error; err == nil { + if transaction.Type == "Income" { + account.Balance -= transaction.Amount + } else { // Expense + account.Balance += transaction.Amount + } + + if err := tx.Save(&account).Error; err != nil { + tx.Rollback() + log.Printf("Error updating account balance for deleted transaction %d: %v", txID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update account balance"}) + return + } + } + } + + // Delete the transaction + if err := tx.Delete(&transaction).Error; err != nil { + tx.Rollback() + log.Printf("Error deleting transaction ID %d for user %d: %v", txID, userID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete transaction"}) + return + } + + // Commit the transaction + if err := tx.Commit().Error; err != nil { + log.Printf("Error committing deletion for transaction %d: %v", txID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save changes"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Transaction deleted successfully"}) +} diff --git a/backend/internal/api/handlers/user_handler.go b/backend/internal/api/handlers/user_handler.go new file mode 100644 index 0000000..aff2d03 --- /dev/null +++ b/backend/internal/api/handlers/user_handler.go @@ -0,0 +1,69 @@ +package handlers + +import ( + "finance/backend/internal/database" + "finance/backend/internal/models" + "net/http" + + "github.com/gin-gonic/gin" +) + +// UserHandler handles user-related operations +type UserHandler struct { +} + +// NewUserHandler creates and returns a new UserHandler instance +func NewUserHandler() *UserHandler { + return &UserHandler{} +} + +// GetCurrentUser returns the authenticated user's information +func (h *UserHandler) GetCurrentUser(c *gin.Context) { + // Get user from context (set by auth middleware) + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"user": user}) +} + +// UpdateCurrentUser updates the authenticated user's information +func (h *UserHandler) UpdateCurrentUser(c *gin.Context) { + userID := c.MustGet("userID").(uint) + var user models.User + + // Fetch the current user + if err := database.DB.First(&user, userID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + // Define update structure + type UpdateUserInput struct { + Name string `json:"name"` + } + + var input UpdateUserInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Update fields if provided + if input.Name != "" { + user.Name = input.Name + } + + // Save the changes + if err := database.DB.Save(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) + return + } + + // Hide sensitive data + user.PasswordHash = "" + + c.JSON(http.StatusOK, gin.H{"user": user}) +} |