aboutsummaryrefslogtreecommitdiffstats
path: root/backend/internal/api/handlers/transaction_handler.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/internal/api/handlers/transaction_handler.go')
-rw-r--r--backend/internal/api/handlers/transaction_handler.go387
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"})
+}