diff options
Diffstat (limited to 'backend/internal/api/handlers/transaction_handler.go')
-rw-r--r-- | backend/internal/api/handlers/transaction_handler.go | 387 |
1 files changed, 387 insertions, 0 deletions
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"}) +} |