Files
notely/backend/internal/application/services/category_service.go
2026-03-26 16:27:14 +00:00

284 lines
8.0 KiB
Go

package services
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/application/dto"
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/domain/entities"
"gitea.hostxtra.co.uk/mrhid6/notely/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)
}