Files
gochat/internal/service/category_service.go
T

267 lines
9.0 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
applogger "github.com/gochat/gochat/pkg/logger"
)
// CategoryService implements business logic for Category CRUD.
type CategoryService struct {
repo *repository.CategoryRepo
relatedRepo *repository.RelatedCategoryRepo
}
func NewCategoryService(repo *repository.CategoryRepo, relatedRepo *repository.RelatedCategoryRepo) *CategoryService {
return &CategoryService{repo: repo, relatedRepo: relatedRepo}
}
// CreateCategoryRequest is the DTO for creating a category.
type CreateCategoryRequest struct {
Name string `json:"name" validate:"required,min=2"`
Slug string `json:"slug" validate:"required"`
Description string `json:"description"`
Icon string `json:"icon"`
Position int `json:"position"`
Locale string `json:"locale"`
ParentID *uint `json:"parent_id"`
ParentCategoryID *uint `json:"parent_category_id"`
AssociatedCategoryID *uint `json:"associated_category_id"`
RelatedCategoryIDs []uint `json:"related_category_ids"`
CustomAttributes json.RawMessage `json:"custom_attributes"`
}
// UpdateCategoryRequest is the DTO for updating a category.
type UpdateCategoryRequest struct {
Name *string `json:"name"`
Slug *string `json:"slug"`
Description *string `json:"description"`
Icon *string `json:"icon"`
Position *int `json:"position"`
Locale *string `json:"locale"`
ParentID *uint `json:"parent_id"`
ParentCategoryID *uint `json:"parent_category_id"`
AssociatedCategoryID *uint `json:"associated_category_id"`
RelatedCategoryIDs []uint `json:"related_category_ids"`
CustomAttributes *json.RawMessage `json:"custom_attributes"`
}
func (s *CategoryService) Create(ctx context.Context, portalID uint, accountID uint, req *CreateCategoryRequest) (*model.Category, error) {
category := &model.Category{
AccountID: accountID,
PortalID: portalID,
Name: req.Name,
Slug: req.Slug,
Description: req.Description,
Icon: req.Icon,
Position: req.Position,
Locale: req.Locale,
ParentID: firstCategoryParentID(req.ParentID, req.ParentCategoryID),
AssociatedCategoryID: req.AssociatedCategoryID,
CustomAttributes: req.CustomAttributes,
}
if err := s.repo.Create(ctx, category); err != nil {
return nil, fmt.Errorf("create category: %w", err)
}
// Handle related categories
if len(req.RelatedCategoryIDs) > 0 {
if err := s.relatedRepo.ReplaceAll(ctx, category.ID, req.RelatedCategoryIDs); err != nil {
applogger.L().Warnf("Failed to set related categories for category %d: %v", category.ID, err)
}
}
return s.repo.GetByPortalAndID(ctx, portalID, category.ID)
}
func (s *CategoryService) GetByID(ctx context.Context, id uint) (*model.Category, error) {
category, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("get category: %w", err)
}
return category, nil
}
func (s *CategoryService) GetByPortalAndID(ctx context.Context, portalID, id uint) (*model.Category, error) {
category, err := s.repo.GetByPortalAndID(ctx, portalID, id)
if err != nil {
return nil, fmt.Errorf("get category: %w", err)
}
return category, nil
}
func (s *CategoryService) GetByPortalSlugAndLocale(ctx context.Context, portalID uint, slug string, locale string) (*model.Category, error) {
category, err := s.repo.FindBySlugAndPortalIDAndLocale(ctx, slug, portalID, locale)
if err != nil {
return nil, fmt.Errorf("get category: %w", err)
}
return category, nil
}
func (s *CategoryService) Update(ctx context.Context, id uint, req *UpdateCategoryRequest) (*model.Category, error) {
category, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("find category: %w", err)
}
if req.Name != nil {
category.Name = *req.Name
}
if req.Slug != nil {
category.Slug = *req.Slug
}
if req.Description != nil {
category.Description = *req.Description
}
if req.Icon != nil {
category.Icon = *req.Icon
}
if req.Position != nil {
category.Position = *req.Position
}
if req.Locale != nil {
category.Locale = *req.Locale
}
if parentID := firstCategoryParentID(req.ParentID, req.ParentCategoryID); parentID != nil {
category.ParentID = parentID
}
if req.AssociatedCategoryID != nil {
category.AssociatedCategoryID = req.AssociatedCategoryID
}
if req.CustomAttributes != nil {
category.CustomAttributes = *req.CustomAttributes
}
if err := s.repo.Update(ctx, category); err != nil {
return nil, fmt.Errorf("update category: %w", err)
}
// Handle related categories — replace all if provided
if req.RelatedCategoryIDs != nil {
if err := s.relatedRepo.ReplaceAll(ctx, category.ID, req.RelatedCategoryIDs); err != nil {
applogger.L().Warnf("Failed to update related categories for category %d: %v", category.ID, err)
}
}
return s.repo.GetByPortalAndID(ctx, category.PortalID, category.ID)
}
func (s *CategoryService) UpdateScoped(ctx context.Context, portalID, id uint, req *UpdateCategoryRequest) (*model.Category, error) {
category, err := s.repo.GetByPortalAndID(ctx, portalID, id)
if err != nil {
return nil, fmt.Errorf("find category: %w", err)
}
return s.UpdateExisting(ctx, category, req)
}
func (s *CategoryService) UpdateExisting(ctx context.Context, category *model.Category, req *UpdateCategoryRequest) (*model.Category, error) {
if category == nil {
return nil, fmt.Errorf("find category: record not found")
}
if req.Name != nil {
category.Name = *req.Name
}
if req.Slug != nil {
category.Slug = *req.Slug
}
if req.Description != nil {
category.Description = *req.Description
}
if req.Icon != nil {
category.Icon = *req.Icon
}
if req.Position != nil {
category.Position = *req.Position
}
if req.Locale != nil {
category.Locale = *req.Locale
}
if parentID := firstCategoryParentID(req.ParentID, req.ParentCategoryID); parentID != nil {
category.ParentID = parentID
}
if req.AssociatedCategoryID != nil {
category.AssociatedCategoryID = req.AssociatedCategoryID
}
if req.CustomAttributes != nil {
category.CustomAttributes = *req.CustomAttributes
}
if err := s.repo.Update(ctx, category); err != nil {
return nil, fmt.Errorf("update category: %w", err)
}
if req.RelatedCategoryIDs != nil {
if err := s.relatedRepo.ReplaceAll(ctx, category.ID, req.RelatedCategoryIDs); err != nil {
applogger.L().Warnf("Failed to update related categories for category %d: %v", category.ID, err)
}
}
return s.repo.GetByPortalAndID(ctx, category.PortalID, category.ID)
}
func (s *CategoryService) Delete(ctx context.Context, id uint) error {
// Check existence first — GORM Delete() returns nil even for non-existent IDs
if _, err := s.repo.GetByID(ctx, id); err != nil {
return fmt.Errorf("find category for delete: %w", err)
}
// Clean up related categories first
if err := s.relatedRepo.DeleteByCategoryID(ctx, id); err != nil {
applogger.L().Warnf("Failed to delete related categories for category %d: %v", id, err)
}
if err := s.repo.Delete(ctx, id); err != nil {
return fmt.Errorf("delete category: %w", err)
}
return nil
}
func (s *CategoryService) DeleteScoped(ctx context.Context, portalID, id uint) error {
if _, err := s.repo.GetByPortalAndID(ctx, portalID, id); err != nil {
return fmt.Errorf("find category for delete: %w", err)
}
if err := s.relatedRepo.DeleteByCategoryID(ctx, id); err != nil {
applogger.L().Warnf("Failed to delete related categories for category %d: %v", id, err)
}
if err := s.repo.Delete(ctx, id); err != nil {
return fmt.Errorf("delete category: %w", err)
}
return nil
}
// ListByPortalID returns categories for a portal with pagination.
func (s *CategoryService) ListByPortalID(ctx context.Context, portalID uint, locale string, page, perPage int) ([]model.Category, int64, error) {
offset := 0
if page > 0 && perPage > 0 {
offset = (page - 1) * perPage
}
if locale != "" {
return s.repo.FindByPortalIDAndLocale(ctx, portalID, locale, offset, perPage)
}
return s.repo.FindByPortalID(ctx, portalID, offset, perPage)
}
func firstCategoryParentID(parentID, parentCategoryID *uint) *uint {
if parentCategoryID != nil {
return parentCategoryID
}
return parentID
}
// Reorder batch-updates category positions.
func (s *CategoryService) Reorder(ctx context.Context, positions map[uint]int) error {
if err := s.repo.UpdatePositions(ctx, positions); err != nil {
return fmt.Errorf("reorder categories: %w", err)
}
return nil
}
func (s *CategoryService) ReorderScoped(ctx context.Context, portalID uint, positions map[uint]int) error {
if err := s.repo.UpdatePositionsForPortal(ctx, portalID, positions); err != nil {
return fmt.Errorf("reorder categories: %w", err)
}
return nil
}