package handler import ( "errors" "strconv" "accounting-app/pkg/api" "accounting-app/internal/models" "accounting-app/internal/service" "github.com/gin-gonic/gin" ) // CategoryHandler handles HTTP requests for category operations type CategoryHandler struct { categoryService *service.CategoryService } // NewCategoryHandler creates a new CategoryHandler instance func NewCategoryHandler(categoryService *service.CategoryService) *CategoryHandler { return &CategoryHandler{ categoryService: categoryService, } } // CreateCategory handles POST /api/v1/categories // Creates a new category with the provided data func (h *CategoryHandler) CreateCategory(c *gin.Context) { var input service.CategoryInput 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) category, err := h.categoryService.CreateCategory(input) if err != nil { if errors.Is(err, service.ErrInvalidParentCategory) { api.BadRequest(c, "Invalid parent category ID") return } if errors.Is(err, service.ErrParentTypeMismatch) { api.BadRequest(c, "Parent category type must match child category type") return } if errors.Is(err, service.ErrParentIsChild) { api.BadRequest(c, "Cannot set a child category as parent (only 2 levels allowed)") return } api.InternalError(c, "Failed to create category: "+err.Error()) return } api.Created(c, category) } // GetCategories handles GET /api/v1/categories // Returns a list of all categories, optionally filtered by type func (h *CategoryHandler) GetCategories(c *gin.Context) { // Check for type filter categoryType := c.Query("type") tree := c.Query("tree") == "true" userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var categories []models.Category var err error if tree { // Return hierarchical tree structure if categoryType != "" { categories, err = h.categoryService.GetCategoryTreeByType(userId.(uint), models.CategoryType(categoryType)) } else { categories, err = h.categoryService.GetCategoryTree(userId.(uint)) } } else { // Return flat list if categoryType != "" { categories, err = h.categoryService.GetCategoriesByType(userId.(uint), models.CategoryType(categoryType)) } else { categories, err = h.categoryService.GetAllCategories(userId.(uint)) } } if err != nil { api.InternalError(c, "Failed to get categories: "+err.Error()) return } api.Success(c, categories) } // GetCategory handles GET /api/v1/categories/:id // Returns a single category by ID func (h *CategoryHandler) GetCategory(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid category ID") return } // Check if children should be included withChildren := c.Query("children") == "true" userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var category *models.Category if withChildren { category, err = h.categoryService.GetCategoryWithChildren(userId.(uint), uint(id)) } else { category, err = h.categoryService.GetCategory(userId.(uint), uint(id)) } if err != nil { if errors.Is(err, service.ErrCategoryNotFound) { api.NotFound(c, "Category not found") return } api.InternalError(c, "Failed to get category: "+err.Error()) return } api.Success(c, category) } // GetCategoryChildren handles GET /api/v1/categories/:id/children // Returns all child categories of a given parent func (h *CategoryHandler) GetCategoryChildren(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid category ID") return } userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } children, err := h.categoryService.GetChildCategories(userId.(uint), uint(id)) if err != nil { if errors.Is(err, service.ErrCategoryNotFound) { api.NotFound(c, "Category not found") return } api.InternalError(c, "Failed to get child categories: "+err.Error()) return } api.Success(c, children) } // UpdateCategory handles PUT /api/v1/categories/:id // Updates an existing category with the provided data func (h *CategoryHandler) UpdateCategory(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid category ID") return } var input service.CategoryInput 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) category, err := h.categoryService.UpdateCategory(userId.(uint), uint(id), input) if err != nil { if errors.Is(err, service.ErrCategoryNotFound) { api.NotFound(c, "Category not found") return } if errors.Is(err, service.ErrInvalidParentCategory) { api.BadRequest(c, "Invalid parent category ID") return } if errors.Is(err, service.ErrParentTypeMismatch) { api.BadRequest(c, "Parent category type must match child category type") return } if errors.Is(err, service.ErrCircularReference) { api.BadRequest(c, "Cannot create circular reference in category hierarchy") return } if errors.Is(err, service.ErrParentIsChild) { api.BadRequest(c, "Cannot set a child category as parent (only 2 levels allowed)") return } api.InternalError(c, "Failed to update category: "+err.Error()) return } api.Success(c, category) } // DeleteCategory handles DELETE /api/v1/categories/:id // Deletes a category by ID func (h *CategoryHandler) DeleteCategory(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid category ID") return } userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } err = h.categoryService.DeleteCategory(userId.(uint), uint(id)) if err != nil { if errors.Is(err, service.ErrCategoryNotFound) { api.NotFound(c, "Category not found") return } if errors.Is(err, service.ErrCategoryInUse) { api.Conflict(c, "Category is in use and cannot be deleted. Please remove associated transactions first.") return } if errors.Is(err, service.ErrCategoryHasChildren) { api.Conflict(c, "Category has child categories and cannot be deleted. Please remove child categories first.") return } api.InternalError(c, "Failed to delete category: "+err.Error()) return } api.NoContent(c) } // RegisterRoutes registers all category routes to the given router group func (h *CategoryHandler) RegisterRoutes(rg *gin.RouterGroup) { categories := rg.Group("/categories") { categories.POST("", h.CreateCategory) categories.GET("", h.GetCategories) categories.GET("/:id", h.GetCategory) categories.GET("/:id/children", h.GetCategoryChildren) categories.PUT("/:id", h.UpdateCategory) categories.DELETE("/:id", h.DeleteCategory) } }