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"}) }