package handler import ( "errors" "strconv" "accounting-app/pkg/api" "accounting-app/internal/service" "github.com/gin-gonic/gin" ) // AccountHandler handles HTTP requests for account operations type AccountHandler struct { accountService *service.AccountService } // NewAccountHandler creates a new AccountHandler instance func NewAccountHandler(accountService *service.AccountService) *AccountHandler { return &AccountHandler{ accountService: accountService, } } // CreateAccount handles POST /api/v1/accounts // Creates a new account with the provided data func (h *AccountHandler) CreateAccount(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var input service.AccountInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } input.UserID = userId.(uint) account, err := h.accountService.CreateAccount(userId.(uint), input) if err != nil { if errors.Is(err, service.ErrNegativeBalanceNotAllowed) { api.BadRequest(c, "Negative balance is not allowed for non-credit accounts") return } api.InternalError(c, "Failed to create account: "+err.Error()) return } api.Created(c, account) } // GetAccounts handles GET /api/v1/accounts // Returns a list of all accounts func (h *AccountHandler) GetAccounts(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } accounts, err := h.accountService.GetAllAccounts(userId.(uint)) if err != nil { api.InternalError(c, "Failed to get accounts: "+err.Error()) return } api.Success(c, accounts) } // GetAccount handles GET /api/v1/accounts/:id // Returns a single account by ID func (h *AccountHandler) GetAccount(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid account ID") return } userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } account, err := h.accountService.GetAccount(userId.(uint), uint(id)) if err != nil { if errors.Is(err, service.ErrAccountNotFound) { api.NotFound(c, "Account not found") return } api.InternalError(c, "Failed to get account: "+err.Error()) return } api.Success(c, account) } // UpdateAccount handles PUT /api/v1/accounts/:id // Updates an existing account with the provided data func (h *AccountHandler) UpdateAccount(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid account ID") return } var input service.AccountInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } // inject userID for other uses if needed but UpdateAccount sig now takes it directly input.UserID = userId.(uint) account, err := h.accountService.UpdateAccount(userId.(uint), uint(id), input) if err != nil { if errors.Is(err, service.ErrAccountNotFound) { api.NotFound(c, "Account not found") return } if errors.Is(err, service.ErrNegativeBalanceNotAllowed) { api.BadRequest(c, "Negative balance is not allowed for non-credit accounts") return } api.InternalError(c, "Failed to update account: "+err.Error()) return } api.Success(c, account) } // DeleteAccount handles DELETE /api/v1/accounts/:id // Deletes an account by ID func (h *AccountHandler) DeleteAccount(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid account ID") return } userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } err = h.accountService.DeleteAccount(userId.(uint), uint(id)) if err != nil { if errors.Is(err, service.ErrAccountNotFound) { api.NotFound(c, "Account not found") return } if errors.Is(err, service.ErrAccountInUse) { api.Conflict(c, "Account is in use and cannot be deleted. Please remove associated transactions first.") return } api.InternalError(c, "Failed to delete account: "+err.Error()) return } api.NoContent(c) } // Transfer handles POST /api/v1/accounts/transfer // Transfers money between two accounts func (h *AccountHandler) Transfer(c *gin.Context) { var input service.TransferInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } input.UserID = userId.(uint) err := h.accountService.Transfer(input.UserID, input.FromAccountID, input.ToAccountID, input.Amount, input.Note) if err != nil { if errors.Is(err, service.ErrSameAccountTransfer) { api.BadRequest(c, "Cannot transfer to the same account") return } if errors.Is(err, service.ErrInvalidTransferAmount) { api.BadRequest(c, "Transfer amount must be positive") return } if errors.Is(err, service.ErrInsufficientBalance) { api.BadRequest(c, "Insufficient balance for this transfer") return } if errors.Is(err, service.ErrAccountNotFound) { api.NotFound(c, "One or both accounts not found") return } api.InternalError(c, "Failed to transfer: "+err.Error()) return } api.Success(c, gin.H{ "message": "Transfer completed successfully", }) } // GetAssetOverview handles GET /api/v1/accounts/overview // Returns the asset overview (total assets, liabilities, net worth) func (h *AccountHandler) GetAssetOverview(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } overview, err := h.accountService.GetAssetOverview(userId.(uint)) if err != nil { api.InternalError(c, "Failed to get asset overview: "+err.Error()) return } api.Success(c, overview) } // ReorderAccounts handles PUT /api/v1/accounts/reorder // Updates the sort order of accounts based on the provided order // Feature: accounting-feature-upgrade // Validates: Requirements 1.3, 1.4 func (h *AccountHandler) ReorderAccounts(c *gin.Context) { var input service.ReorderAccountsInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } if len(input.AccountIDs) == 0 { api.BadRequest(c, "Account IDs array cannot be empty") return } err := h.accountService.ReorderAccounts(userId.(uint), input.AccountIDs) if err != nil { if errors.Is(err, service.ErrAccountNotFound) { api.NotFound(c, "One or more accounts not found") return } api.InternalError(c, "Failed to reorder accounts: "+err.Error()) return } api.Success(c, gin.H{ "message": "Accounts reordered successfully", }) } // RegisterRoutes registers all account routes to the given router group func (h *AccountHandler) RegisterRoutes(rg *gin.RouterGroup) { accounts := rg.Group("/accounts") { accounts.POST("", h.CreateAccount) accounts.GET("", h.GetAccounts) accounts.GET("/overview", h.GetAssetOverview) accounts.POST("/transfer", h.Transfer) accounts.PUT("/reorder", h.ReorderAccounts) accounts.GET("/:id", h.GetAccount) accounts.PUT("/:id", h.UpdateAccount) accounts.DELETE("/:id", h.DeleteAccount) } }