aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatarLibravatar Biswa Kalyan Bhuyan <biswa@surgot.in> 2025-04-26 01:06:54 +0530
committerLibravatarLibravatar Biswa Kalyan Bhuyan <biswa@surgot.in> 2025-04-26 01:06:54 +0530
commit9d65a782ca3e2084ef0f560500f6014d7bd09bc0 (patch)
treee97195e8b967267d8d40098ae40a940fa2d44571
parent84622698f6c0e9d76ebe434c00df587908a37015 (diff)
downloadfinance-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.go59
-rw-r--r--backend/internal/api/handlers/account_handler.go162
-rw-r--r--backend/internal/api/handlers/auth_handlers.go38
-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.go239
-rw-r--r--backend/internal/api/handlers/transaction_handler.go387
-rw-r--r--backend/internal/api/handlers/user_handler.go69
-rw-r--r--backend/internal/router/router.go49
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)
}
}