package services import ( "context" "errors" "time" "go.mongodb.org/mongo-driver/v2/bson" "github.com/noteapp/backend/internal/application/dto" "github.com/noteapp/backend/internal/domain/entities" "github.com/noteapp/backend/internal/domain/repositories" ) // CategoryService handles category operations type CategoryService struct { categoryRepo repositories.CategoryRepository membershipRepo repositories.MembershipRepository noteRepo repositories.NoteRepository permissionService *PermissionService } // NewCategoryService creates a new category service func NewCategoryService( categoryRepo repositories.CategoryRepository, membershipRepo repositories.MembershipRepository, noteRepo repositories.NoteRepository, permissionService *PermissionService, ) *CategoryService { return &CategoryService{ categoryRepo: categoryRepo, membershipRepo: membershipRepo, noteRepo: noteRepo, permissionService: permissionService, } } // CreateCategory creates a new category func (s *CategoryService) CreateCategory(ctx context.Context, spaceID, userID bson.ObjectID, req *dto.CreateCategoryRequest) (*dto.CategoryDTO, error) { // Verify user has access to space membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID) if err != nil { return nil, errors.New("unauthorized") } _ = membership hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "category.create") if permErr != nil { return nil, permErr } if !hasPermission { return nil, errors.New("insufficient permissions") } var parentID *bson.ObjectID if req.ParentID != nil { id, _ := bson.ObjectIDFromHex(*req.ParentID) parentID = &id // Verify parent category exists and belongs to same space parent, err := s.categoryRepo.GetCategoryByID(ctx, id) if err != nil || parent.SpaceID != spaceID { return nil, errors.New("invalid parent category") } } // Get next order value categories, err := s.categoryRepo.GetCategoriesBySpaceID(ctx, spaceID) order := len(categories) category := &entities.Category{ SpaceID: spaceID, Name: req.Name, Description: req.Description, ParentID: parentID, Icon: req.Icon, Order: order, CreatedBy: userID, UpdatedBy: userID, } if err := s.categoryRepo.CreateCategory(ctx, category); err != nil { return nil, err } return dto.NewCategoryDTO(category), nil } // GetCategoryTree retrieves the full tree structure for a space func (s *CategoryService) GetCategoryTree(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.CategoryTreeDTO, error) { // Verify user has access to space if _, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID); err != nil { return nil, errors.New("unauthorized") } // Get root categories categories, err := s.categoryRepo.GetRootCategories(ctx, spaceID) if err != nil { return nil, err } var trees []*dto.CategoryTreeDTO for _, category := range categories { tree, err := s.buildCategoryTree(ctx, category, spaceID) if err == nil { trees = append(trees, tree) } } return trees, nil } // buildCategoryTree recursively builds a category tree func (s *CategoryService) buildCategoryTree(ctx context.Context, category *entities.Category, spaceID bson.ObjectID) (*dto.CategoryTreeDTO, error) { tree := &dto.CategoryTreeDTO{ CategoryDTO: dto.NewCategoryDTO(category), } // Get subcategories subcategories, err := s.categoryRepo.GetSubcategories(ctx, category.ID) if err == nil { for _, subcat := range subcategories { subtree, err := s.buildCategoryTree(ctx, subcat, spaceID) if err == nil { tree.Subcategories = append(tree.Subcategories, subtree) } } } // Get notes in this category notes, err := s.noteRepo.GetNotesByCategory(ctx, spaceID, category.ID) if err == nil { for _, note := range notes { tree.Notes = append(tree.Notes, dto.NewNoteListItemDTO(note)) } } return tree, nil } // UpdateCategory updates a category func (s *CategoryService) UpdateCategory(ctx context.Context, categoryID, spaceID, userID bson.ObjectID, req *dto.UpdateCategoryRequest) (*dto.CategoryDTO, error) { // Verify user has access to space membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID) if err != nil { return nil, errors.New("unauthorized") } _ = membership hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "category.edit") if permErr != nil { return nil, permErr } if !hasPermission { return nil, errors.New("insufficient permissions") } category, err := s.categoryRepo.GetCategoryByID(ctx, categoryID) if err != nil { return nil, err } // Verify category belongs to this space if category.SpaceID != spaceID { return nil, errors.New("category not found in this space") } if req.Name != "" { category.Name = req.Name } if req.Description != "" { category.Description = req.Description } if req.Icon != "" { category.Icon = req.Icon } category.UpdatedBy = userID if err := s.categoryRepo.UpdateCategory(ctx, category); err != nil { return nil, err } return dto.NewCategoryDTO(category), nil } // DeleteCategory deletes a category (and optionally move notes) func (s *CategoryService) DeleteCategory(ctx context.Context, categoryID, spaceID, userID bson.ObjectID, moveNotesTo *string) error { // Verify user has access to space membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID) if err != nil { return errors.New("unauthorized") } _ = membership hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "category.delete") if permErr != nil { return permErr } if !hasPermission { return errors.New("insufficient permissions") } category, err := s.categoryRepo.GetCategoryByID(ctx, categoryID) if err != nil { return err } // Verify category belongs to this space if category.SpaceID != spaceID { return errors.New("category not found in this space") } // Handle notes in this category notes, err := s.noteRepo.GetNotesByCategory(ctx, spaceID, categoryID) if err == nil { for _, note := range notes { if moveNotesTo != nil { targetID, _ := bson.ObjectIDFromHex(*moveNotesTo) note.CategoryID = &targetID s.noteRepo.UpdateNote(ctx, note) } else { // Move to root (no category) note.CategoryID = nil s.noteRepo.UpdateNote(ctx, note) } } } return s.categoryRepo.DeleteCategory(ctx, categoryID) } // MoveCategory moves a category to a new parent func (s *CategoryService) MoveCategory(ctx context.Context, categoryID, spaceID, userID bson.ObjectID, newParentID *string) (*dto.CategoryDTO, error) { // Verify user has access to space membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID) if err != nil { return nil, errors.New("unauthorized") } _ = membership hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "category.edit") if permErr != nil { return nil, permErr } if !hasPermission { return nil, errors.New("insufficient permissions") } category, err := s.categoryRepo.GetCategoryByID(ctx, categoryID) if err != nil { return nil, err } // Verify category belongs to this space if category.SpaceID != spaceID { return nil, errors.New("category not found in this space") } // Validate new parent if newParentID != nil { parentID, _ := bson.ObjectIDFromHex(*newParentID) parent, err := s.categoryRepo.GetCategoryByID(ctx, parentID) if err != nil || parent.SpaceID != spaceID { return nil, errors.New("invalid parent category") } category.ParentID = &parentID } else { category.ParentID = nil } category.UpdatedBy = userID category.UpdatedAt = time.Now() if err := s.categoryRepo.UpdateCategory(ctx, category); err != nil { return nil, err } return dto.NewCategoryDTO(category), nil } func (s *CategoryService) hasSpacePermission(ctx context.Context, userID, spaceID bson.ObjectID, action string) (bool, error) { if s.permissionService == nil { return false, errors.New("permission service unavailable") } return s.permissionService.HasSpacePermission(ctx, userID, spaceID, action) }