package transactions import ( "net/http" "strconv" "time" "finance/backend/internal/database" "finance/backend/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // GetTransactions returns all transactions for the authenticated user // Can be filtered by type (income/expense) and date range func GetTransactions() gin.HandlerFunc { return func(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) var transactions []models.Transaction // Build query query := database.DB.Where("user_id = ?", userObj.ID) // Filter by transaction type if provided if transactionType := c.Query("type"); transactionType != "" { if transactionType != "Income" && transactionType != "Expense" { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid transaction type. Use 'Income' or 'Expense'"}) return } query = query.Where("type = ?", transactionType) } // Filter by account if provided if accountID := c.Query("account_id"); accountID != "" { query = query.Where("account_id = ?", accountID) } // Filter by category if provided if category := c.Query("category"); category != "" { query = query.Where("category = ?", category) } // Filter by start date if provided if startDate := c.Query("start_date"); startDate != "" { date, err := time.Parse("2006-01-02", startDate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start date format. Use YYYY-MM-DD"}) return } query = query.Where("date >= ?", date) } // Filter by end date if provided if endDate := c.Query("end_date"); endDate != "" { date, err := time.Parse("2006-01-02", endDate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end date format. Use YYYY-MM-DD"}) return } // Add a day to include all transactions on the end date date = date.Add(24 * time.Hour) query = query.Where("date < ?", date) } // Order by date (newest first) query = query.Order("date DESC") // Execute query if err := query.Find(&transactions).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch transactions"}) return } c.JSON(http.StatusOK, gin.H{"transactions": transactions}) } } // GetTransactionByID returns a specific transaction by ID func GetTransactionByID() gin.HandlerFunc { return func(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) // Get transaction ID from URL parameter transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid transaction ID format"}) return } var transaction models.Transaction // Fetch the transaction and ensure it belongs to the authenticated user if err := database.DB.Where("id = ? AND user_id = ?", transactionID, userObj.ID).First(&transaction).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch transaction"}) } return } c.JSON(http.StatusOK, gin.H{"transaction": transaction}) } } // CreateTransaction creates a new transaction func CreateTransaction() gin.HandlerFunc { return func(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) // Define a struct to bind the request JSON var input struct { 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" binding:"required"` AccountID *uint `json:"accountId"` } // Bind JSON to struct 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": "Transaction type must be 'Income' or 'Expense'"}) return } // Parse 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: userObj.ID, Description: input.Description, Amount: input.Amount, Type: input.Type, Date: date, Category: input.Category, AccountID: input.AccountID, } // If an account is specified, verify that it exists and belongs to the user if input.AccountID != nil { var account models.Account if err := database.DB.Where("id = ? AND user_id = ?", *input.AccountID, userObj.ID).First(&account).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusBadRequest, gin.H{"error": "Specified account not found or does not belong to you"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify account"}) } return } // Update account balance based on transaction type // For income, add to the balance; for expense, subtract from the balance tx := database.DB.Begin() if input.Type == "Income" { account.Balance += input.Amount } else { // Expense account.Balance -= input.Amount } // Save updated account balance if err := tx.Save(&account).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update account balance"}) return } // Save transaction if err := tx.Create(&transaction).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create transaction"}) return } // Commit transaction if err := tx.Commit().Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction"}) return } } else { // No account specified, just save the transaction if err := database.DB.Create(&transaction).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create transaction"}) return } } c.JSON(http.StatusCreated, gin.H{"transaction": transaction}) } } // UpdateTransaction updates an existing transaction func UpdateTransaction() gin.HandlerFunc { return func(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) // Get transaction ID from URL parameter transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid transaction ID format"}) return } // Find existing transaction var transaction models.Transaction if err := database.DB.Where("id = ? AND user_id = ?", transactionID, userObj.ID).First(&transaction).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch transaction"}) } return } // Store original values for comparison originalAccountID := transaction.AccountID originalAmount := transaction.Amount originalType := transaction.Type // Define a struct to bind the request JSON var input struct { 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"` AccountID *uint `json:"accountId"` } // Bind JSON to struct if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Begin database transaction for updating both transaction and account balance tx := database.DB.Begin() // Revert original account balance if the transaction affected an account if originalAccountID != nil { var originalAccount models.Account if err := tx.First(&originalAccount, *originalAccountID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch original account"}) return } // Reverse the effect of the original transaction if originalType == "Income" { originalAccount.Balance -= originalAmount } else { // Expense originalAccount.Balance += originalAmount } if err := tx.Save(&originalAccount).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update original account balance"}) return } } // Update transaction 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": "Transaction type must be '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 || c.GetHeader("Content-Type") == "application/json" { transaction.AccountID = input.AccountID } // Update new account balance if the transaction affects an account if transaction.AccountID != nil { var newAccount models.Account if err := tx.First(&newAccount, *transaction.AccountID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch new account"}) return } // Apply the effect of the updated transaction if transaction.Type == "Income" { newAccount.Balance += transaction.Amount } else { // Expense newAccount.Balance -= transaction.Amount } if err := tx.Save(&newAccount).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update new account balance"}) return } } // Save transaction if err := tx.Save(&transaction).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update transaction"}) return } // Commit transaction if err := tx.Commit().Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit changes"}) return } c.JSON(http.StatusOK, gin.H{"transaction": transaction}) } } // DeleteTransaction deletes a transaction func DeleteTransaction() gin.HandlerFunc { return func(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) // Get transaction ID from URL parameter transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid transaction ID format"}) return } // Find the transaction var transaction models.Transaction if err := database.DB.Where("id = ? AND user_id = ?", transactionID, userObj.ID).First(&transaction).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch transaction"}) } return } // Begin database transaction tx := database.DB.Begin() // Update account balance if this transaction was linked to an account if transaction.AccountID != nil { var account models.Account if err := tx.First(&account, *transaction.AccountID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch account"}) return } // Reverse the effect of the transaction on the account balance if transaction.Type == "Income" { account.Balance -= transaction.Amount } else { // Expense account.Balance += transaction.Amount } if err := tx.Save(&account).Error; err != nil { tx.Rollback() 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() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete transaction"}) return } // Commit transaction if err := tx.Commit().Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit changes"}) return } c.JSON(http.StatusOK, gin.H{"message": "Transaction deleted successfully"}) } }