diff options
author | 2025-04-26 01:06:54 +0530 | |
---|---|---|
committer | 2025-04-26 01:06:54 +0530 | |
commit | 9d65a782ca3e2084ef0f560500f6014d7bd09bc0 (patch) | |
tree | e97195e8b967267d8d40098ae40a940fa2d44571 | |
parent | 84622698f6c0e9d76ebe434c00df587908a37015 (diff) | |
download | finance-9d65a782ca3e2084ef0f560500f6014d7bd09bc0.tar.gz finance-9d65a782ca3e2084ef0f560500f6014d7bd09bc0.tar.bz2 finance-9d65a782ca3e2084ef0f560500f6014d7bd09bc0.zip |
finance/backend: mvfeat: moved the backend api handlers to api/handlers and added couple of more api request handlers
-rw-r--r-- | backend/cmd/api/main.go | 59 | ||||
-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 (renamed from backend/handlers/goal_handler.go) | 0 | ||||
-rw-r--r-- | backend/internal/api/handlers/goal_handler_test.go (renamed from backend/handlers/goal_handler_test.go) | 0 | ||||
-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 | ||||
-rw-r--r-- | backend/internal/router/router.go | 49 |
9 files changed, 940 insertions, 63 deletions
diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index f882175..4884d12 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -6,12 +6,12 @@ import ( "os" "time" - "finance/backend/handlers" "finance/backend/internal/config" "finance/backend/internal/database" "finance/backend/internal/logger" "finance/backend/internal/middleware" "finance/backend/internal/models" + "finance/backend/internal/router" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" @@ -53,17 +53,8 @@ func main() { log.Fatal("Failed to migrate database:", err) } - // Initialize handlers - goalHandler := handlers.NewGoalHandler() - // Initialize other handlers as needed - // userHandler := handlers.NewUserHandler() - // accountHandler := handlers.NewAccountHandler() - // transactionHandler := handlers.NewTransactionHandler() - // loanHandler := handlers.NewLoanHandler() - // authHandler := handlers.NewAuthHandler() - - // Create a new Gin router without default middleware - r := gin.New() + // Initialize the router from the internal/router package + r := router.SetupRouter(cfg) // Add custom middleware r.Use(middleware.RequestLogger(log)) @@ -102,50 +93,6 @@ func main() { c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "Database connection is healthy"}) }) - // API v1 routes - v1 := r.Group("/api/v1") - - // Authentication routes (no JWT required) - // v1.POST("/register", authHandler.Register) - // v1.POST("/login", authHandler.Login) - - // Protected routes (JWT required) - protected := v1.Group("") - // protected.Use(authHandler.JWTAuth) - - // User routes - // protected.GET("/users/me", userHandler.GetCurrentUser) - // protected.PUT("/users/me", userHandler.UpdateCurrentUser) - - // Account routes - // protected.GET("/accounts", accountHandler.GetAccounts) - // protected.GET("/accounts/:id", accountHandler.GetAccountByID) - // protected.POST("/accounts", accountHandler.CreateAccount) - // protected.PUT("/accounts/:id", accountHandler.UpdateAccount) - // protected.DELETE("/accounts/:id", accountHandler.DeleteAccount) - - // Transaction routes - // protected.GET("/transactions", transactionHandler.GetTransactions) - // protected.GET("/transactions/:id", transactionHandler.GetTransactionByID) - // protected.POST("/transactions", transactionHandler.CreateTransaction) - // protected.PUT("/transactions/:id", transactionHandler.UpdateTransaction) - // protected.DELETE("/transactions/:id", transactionHandler.DeleteTransaction) - - // Goal routes - protected.GET("/goals", goalHandler.GetGoals) - protected.GET("/goals/:id", goalHandler.GetGoal) - protected.POST("/goals", goalHandler.CreateGoal) - protected.PUT("/goals/:id", goalHandler.UpdateGoal) - protected.DELETE("/goals/:id", goalHandler.DeleteGoal) - protected.PATCH("/goals/:id/progress", goalHandler.UpdateGoalProgress) - - // Loan routes - // protected.GET("/loans", loanHandler.GetLoans) - // protected.GET("/loans/:id", loanHandler.GetLoanByID) - // protected.POST("/loans", loanHandler.CreateLoan) - // protected.PUT("/loans/:id", loanHandler.UpdateLoan) - // protected.DELETE("/loans/:id", loanHandler.DeleteLoan) - // Start server serverAddr := fmt.Sprintf("%s:%d", cfg.ServerHost, cfg.ServerPort) log.Info("Server starting on", serverAddr) 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/handlers/goal_handler.go b/backend/internal/api/handlers/goal_handler.go index 09bea74..09bea74 100644 --- a/backend/handlers/goal_handler.go +++ b/backend/internal/api/handlers/goal_handler.go diff --git a/backend/handlers/goal_handler_test.go b/backend/internal/api/handlers/goal_handler_test.go index d91c000..d91c000 100644 --- a/backend/handlers/goal_handler_test.go +++ b/backend/internal/api/handlers/goal_handler_test.go 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}) +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index b82e1af..3a4d413 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -1,8 +1,7 @@ package router import ( - "finance/backend/internal/api/auth" - "finance/backend/internal/api/v1/users" + "finance/backend/internal/api/handlers" "finance/backend/internal/config" "finance/backend/internal/database" "net/http" @@ -66,21 +65,57 @@ func SetupRouter(cfg *config.Config) *gin.Engine { }) }) + // Initialize handlers + authHandler := handlers.NewAuthHandler(cfg) + goalHandler := handlers.NewGoalHandler() + userHandler := handlers.NewUserHandler() + accountHandler := handlers.NewAccountHandler() + transactionHandler := handlers.NewTransactionHandler() + loanHandler := handlers.NewLoanHandler() + // API v1 routes v1 := r.Group("/api/v1") { // Auth routes (public) - v1.POST("/auth/signup", auth.Signup(cfg)) - v1.POST("/auth/login", auth.Login(cfg)) + v1.POST("/auth/signup", authHandler.Register) + v1.POST("/auth/login", authHandler.Login) // Protected routes protected := v1.Group("") - protected.Use(auth.AuthMiddleware(cfg)) + protected.Use(authHandler.JWTAuth) { // User routes - protected.GET("/users/me", users.GetCurrentUser) + protected.GET("/users/me", userHandler.GetCurrentUser) + protected.PUT("/users/me", userHandler.UpdateCurrentUser) + + // Account routes + protected.GET("/accounts", accountHandler.GetAccounts) + protected.GET("/accounts/:id", accountHandler.GetAccountByID) + protected.POST("/accounts", accountHandler.CreateAccount) + protected.PUT("/accounts/:id", accountHandler.UpdateAccount) + protected.DELETE("/accounts/:id", accountHandler.DeleteAccount) + + // Transaction routes + protected.GET("/transactions", transactionHandler.GetTransactions) + protected.GET("/transactions/:id", transactionHandler.GetTransactionByID) + protected.POST("/transactions", transactionHandler.CreateTransaction) + protected.PUT("/transactions/:id", transactionHandler.UpdateTransaction) + protected.DELETE("/transactions/:id", transactionHandler.DeleteTransaction) + + // Goal routes + protected.GET("/goals", goalHandler.GetGoals) + protected.GET("/goals/:id", goalHandler.GetGoal) + protected.POST("/goals", goalHandler.CreateGoal) + protected.PUT("/goals/:id", goalHandler.UpdateGoal) + protected.DELETE("/goals/:id", goalHandler.DeleteGoal) + protected.PATCH("/goals/:id/progress", goalHandler.UpdateGoalProgress) - // Add other protected routes here + // Loan routes + protected.GET("/loans", loanHandler.GetLoans) + protected.GET("/loans/:id", loanHandler.GetLoanByID) + protected.POST("/loans", loanHandler.CreateLoan) + protected.PUT("/loans/:id", loanHandler.UpdateLoan) + protected.DELETE("/loans/:id", loanHandler.DeleteLoan) } } |