init
This commit is contained in:
313
internal/service/category_service.go
Normal file
313
internal/service/category_service.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"accounting-app/internal/models"
|
||||
"accounting-app/internal/repository"
|
||||
)
|
||||
|
||||
// Category service errors
|
||||
var (
|
||||
ErrCategoryNotFound = errors.New("category not found")
|
||||
ErrCategoryInUse = errors.New("category is in use and cannot be deleted")
|
||||
ErrCategoryHasChildren = errors.New("category has children and cannot be deleted")
|
||||
ErrInvalidParentCategory = errors.New("invalid parent category")
|
||||
ErrParentTypeMismatch = errors.New("parent category type must match child category type")
|
||||
ErrCircularReference = errors.New("circular reference detected in category hierarchy")
|
||||
ErrParentIsChild = errors.New("cannot set a child category as parent")
|
||||
)
|
||||
|
||||
// CategoryInput represents the input data for creating or updating a category
|
||||
type CategoryInput struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Icon string `json:"icon"`
|
||||
Type models.CategoryType `json:"type" binding:"required"`
|
||||
ParentID *uint `json:"parent_id,omitempty"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// CategoryService handles business logic for categories
|
||||
type CategoryService struct {
|
||||
repo *repository.CategoryRepository
|
||||
}
|
||||
|
||||
// NewCategoryService creates a new CategoryService instance
|
||||
func NewCategoryService(repo *repository.CategoryRepository) *CategoryService {
|
||||
return &CategoryService{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCategory creates a new category with business logic validation
|
||||
func (s *CategoryService) CreateCategory(input CategoryInput) (*models.Category, error) {
|
||||
// Validate parent category if provided
|
||||
if input.ParentID != nil {
|
||||
parent, err := s.repo.GetByID(input.UserID, *input.ParentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return nil, ErrInvalidParentCategory
|
||||
}
|
||||
return nil, fmt.Errorf("failed to validate parent category: %w", err)
|
||||
}
|
||||
// userID check handled by repo
|
||||
|
||||
// Ensure parent category type matches the new category type
|
||||
if parent.Type != input.Type {
|
||||
return nil, ErrParentTypeMismatch
|
||||
}
|
||||
|
||||
// Ensure parent is not already a child (only allow 2 levels)
|
||||
if parent.ParentID != nil {
|
||||
return nil, ErrParentIsChild
|
||||
}
|
||||
}
|
||||
|
||||
// Create the category model
|
||||
category := &models.Category{
|
||||
UserID: input.UserID,
|
||||
Name: input.Name,
|
||||
Icon: input.Icon,
|
||||
Type: input.Type,
|
||||
ParentID: input.ParentID,
|
||||
SortOrder: input.SortOrder,
|
||||
}
|
||||
|
||||
// Save to database
|
||||
if err := s.repo.Create(category); err != nil {
|
||||
return nil, fmt.Errorf("failed to create category: %w", err)
|
||||
}
|
||||
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// GetCategory retrieves a category by ID and verifies ownership
|
||||
func (s *CategoryService) GetCategory(userID, id uint) (*models.Category, error) {
|
||||
category, err := s.repo.GetByID(userID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get category: %w", err)
|
||||
}
|
||||
// userID check handled by repo
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// GetCategoryWithChildren retrieves a category with its children and verifies ownership
|
||||
func (s *CategoryService) GetCategoryWithChildren(userID, id uint) (*models.Category, error) {
|
||||
category, err := s.repo.GetWithChildren(userID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get category with children: %w", err)
|
||||
}
|
||||
// userID check handled by repo
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// GetAllCategories retrieves all categories for a user
|
||||
func (s *CategoryService) GetAllCategories(userID uint) ([]models.Category, error) {
|
||||
categories, err := s.repo.GetAll(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get categories: %w", err)
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// GetCategoriesByType retrieves all categories of a specific type for a user
|
||||
func (s *CategoryService) GetCategoriesByType(userID uint, categoryType models.CategoryType) ([]models.Category, error) {
|
||||
categories, err := s.repo.GetByType(userID, categoryType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get categories by type: %w", err)
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// GetCategoryTree retrieves all categories in a hierarchical tree structure for a user
|
||||
// Returns only root categories with their children preloaded
|
||||
func (s *CategoryService) GetCategoryTree(userID uint) ([]models.Category, error) {
|
||||
categories, err := s.repo.GetAllWithChildren(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get category tree: %w", err)
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// GetCategoryTreeByType retrieves categories of a specific type in a hierarchical tree structure for a user
|
||||
func (s *CategoryService) GetCategoryTreeByType(userID uint, categoryType models.CategoryType) ([]models.Category, error) {
|
||||
// Get root categories of the specified type
|
||||
rootCategories, err := s.repo.GetRootCategoriesByType(userID, categoryType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get root categories by type: %w", err)
|
||||
}
|
||||
|
||||
// Load children for each root category
|
||||
for i := range rootCategories {
|
||||
children, err := s.repo.GetChildren(userID, rootCategories[i].ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get children for category %d: %w", rootCategories[i].ID, err)
|
||||
}
|
||||
rootCategories[i].Children = children
|
||||
}
|
||||
|
||||
return rootCategories, nil
|
||||
}
|
||||
|
||||
// GetRootCategories retrieves all root categories (categories without parent) for a user
|
||||
func (s *CategoryService) GetRootCategories(userID uint) ([]models.Category, error) {
|
||||
categories, err := s.repo.GetRootCategories(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get root categories: %w", err)
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// GetChildCategories retrieves all child categories of a given parent
|
||||
func (s *CategoryService) GetChildCategories(userID, parentID uint) ([]models.Category, error) {
|
||||
// Verify parent exists
|
||||
_, err := s.repo.GetByID(userID, parentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to verify parent category: %w", err)
|
||||
}
|
||||
// userID check handled by repo
|
||||
|
||||
children, err := s.repo.GetChildren(userID, parentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get child categories: %w", err)
|
||||
}
|
||||
return children, nil
|
||||
}
|
||||
|
||||
// UpdateCategory updates an existing category after verifying ownership
|
||||
func (s *CategoryService) UpdateCategory(userID, id uint, input CategoryInput) (*models.Category, error) {
|
||||
// Get existing category
|
||||
category, err := s.repo.GetByID(userID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get category: %w", err)
|
||||
}
|
||||
// userID check handled by repo
|
||||
|
||||
// Validate parent category if provided
|
||||
if input.ParentID != nil {
|
||||
// Cannot set self as parent
|
||||
if *input.ParentID == id {
|
||||
return nil, ErrCircularReference
|
||||
}
|
||||
|
||||
parent, err := s.repo.GetByID(userID, *input.ParentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return nil, ErrInvalidParentCategory
|
||||
}
|
||||
return nil, fmt.Errorf("failed to validate parent category: %w", err)
|
||||
}
|
||||
// userID check handled by repo
|
||||
|
||||
// Ensure parent category type matches
|
||||
if parent.Type != input.Type {
|
||||
return nil, ErrParentTypeMismatch
|
||||
}
|
||||
|
||||
// Ensure parent is not already a child (only allow 2 levels)
|
||||
if parent.ParentID != nil {
|
||||
return nil, ErrParentIsChild
|
||||
}
|
||||
|
||||
// Check if the new parent is a child of the current category (circular reference)
|
||||
if parent.ParentID != nil && *parent.ParentID == id {
|
||||
return nil, ErrCircularReference
|
||||
}
|
||||
}
|
||||
|
||||
// If this category has children and we're trying to set a parent, reject
|
||||
// (would create more than 2 levels)
|
||||
if input.ParentID != nil {
|
||||
children, err := s.repo.GetChildren(userID, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check children: %w", err)
|
||||
}
|
||||
if len(children) > 0 {
|
||||
return nil, ErrParentIsChild
|
||||
}
|
||||
}
|
||||
|
||||
// Update fields
|
||||
category.Name = input.Name
|
||||
category.Icon = input.Icon
|
||||
category.Type = input.Type
|
||||
category.ParentID = input.ParentID
|
||||
category.SortOrder = input.SortOrder
|
||||
|
||||
// Save to database
|
||||
if err := s.repo.Update(category); err != nil {
|
||||
return nil, fmt.Errorf("failed to update category: %w", err)
|
||||
}
|
||||
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// DeleteCategory deletes a category by ID after verifying ownership
|
||||
func (s *CategoryService) DeleteCategory(userID, id uint) error {
|
||||
_, err := s.repo.GetByID(userID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return ErrCategoryNotFound
|
||||
}
|
||||
return fmt.Errorf("failed to check category existence: %w", err)
|
||||
}
|
||||
// userID check handled by repo
|
||||
|
||||
err = s.repo.Delete(userID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return ErrCategoryNotFound
|
||||
}
|
||||
if errors.Is(err, repository.ErrCategoryInUse) {
|
||||
return ErrCategoryInUse
|
||||
}
|
||||
if errors.Is(err, repository.ErrCategoryHasChildren) {
|
||||
return ErrCategoryHasChildren
|
||||
}
|
||||
return fmt.Errorf("failed to delete category: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CategoryExists checks if a category exists by ID
|
||||
func (s *CategoryService) CategoryExists(userID uint, id uint) (bool, error) {
|
||||
exists, err := s.repo.ExistsByID(userID, id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check category existence: %w", err)
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// GetCategoryPath returns the full path of a category (parent -> child)
|
||||
func (s *CategoryService) GetCategoryPath(userID, id uint) ([]models.Category, error) {
|
||||
category, err := s.repo.GetWithParent(userID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get category: %w", err)
|
||||
}
|
||||
// userID check handled by repo
|
||||
|
||||
path := []models.Category{}
|
||||
if category.Parent != nil {
|
||||
path = append(path, *category.Parent)
|
||||
}
|
||||
path = append(path, *category)
|
||||
|
||||
return path, nil
|
||||
}
|
||||
Reference in New Issue
Block a user