This commit is contained in:
2026-01-25 21:59:00 +08:00
parent 7fd537bef3
commit 4cad3f0250
118 changed files with 30473 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
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)
}
}