feat(help-center): align category payloads
This commit is contained in:
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
// CategoryService implements business logic for Category CRUD.
|
||||
type CategoryService struct {
|
||||
repo *repository.CategoryRepo
|
||||
relatedRepo *repository.RelatedCategoryRepo
|
||||
repo *repository.CategoryRepo
|
||||
relatedRepo *repository.RelatedCategoryRepo
|
||||
}
|
||||
|
||||
func NewCategoryService(repo *repository.CategoryRepo, relatedRepo *repository.RelatedCategoryRepo) *CategoryService {
|
||||
@@ -22,44 +22,47 @@ func NewCategoryService(repo *repository.CategoryRepo, relatedRepo *repository.R
|
||||
|
||||
// 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"`
|
||||
AssociatedCategoryID *uint `json:"associated_category_id"`
|
||||
RelatedCategoryIDs []uint `json:"related_category_ids"`
|
||||
CustomAttributes json.RawMessage `json:"custom_attributes"`
|
||||
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"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
Position *int `json:"position"`
|
||||
Locale string `json:"locale"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
AssociatedCategoryID *uint `json:"associated_category_id"`
|
||||
RelatedCategoryIDs []uint `json:"related_category_ids"`
|
||||
CustomAttributes json.RawMessage `json:"custom_attributes"`
|
||||
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: req.ParentID,
|
||||
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,
|
||||
CustomAttributes: req.CustomAttributes,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(ctx, category); err != nil {
|
||||
@@ -73,7 +76,7 @@ func (s *CategoryService) Create(ctx context.Context, portalID uint, accountID u
|
||||
}
|
||||
}
|
||||
|
||||
return category, nil
|
||||
return s.repo.GetByPortalAndID(ctx, portalID, category.ID)
|
||||
}
|
||||
|
||||
func (s *CategoryService) GetByID(ctx context.Context, id uint) (*model.Category, error) {
|
||||
@@ -84,35 +87,46 @@ func (s *CategoryService) GetByID(ctx context.Context, id uint) (*model.Category
|
||||
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) 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 != "" {
|
||||
category.Name = req.Name
|
||||
if req.Name != nil {
|
||||
category.Name = *req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
category.Description = req.Description
|
||||
if req.Slug != nil {
|
||||
category.Slug = *req.Slug
|
||||
}
|
||||
if req.Icon != "" {
|
||||
category.Icon = req.Icon
|
||||
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 != "" {
|
||||
category.Locale = req.Locale
|
||||
if req.Locale != nil {
|
||||
category.Locale = *req.Locale
|
||||
}
|
||||
if req.ParentID != nil {
|
||||
category.ParentID = req.ParentID
|
||||
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
|
||||
category.CustomAttributes = *req.CustomAttributes
|
||||
}
|
||||
|
||||
if err := s.repo.Update(ctx, category); err != nil {
|
||||
@@ -126,7 +140,57 @@ func (s *CategoryService) Update(ctx context.Context, id uint, req *UpdateCatego
|
||||
}
|
||||
}
|
||||
|
||||
return category, nil
|
||||
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 {
|
||||
@@ -146,19 +210,49 @@ func (s *CategoryService) Delete(ctx context.Context, id uint) error {
|
||||
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 := (page - 1) * perPage
|
||||
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
|
||||
}
|
||||
|
||||
@@ -95,12 +95,16 @@ func TestCategoryService_Update(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
newPos := 5
|
||||
newName := "User Guides"
|
||||
newDescription := "Updated description"
|
||||
newIcon := "📖"
|
||||
newLocale := "zh"
|
||||
updateReq := &UpdateCategoryRequest{
|
||||
Name: "User Guides",
|
||||
Description: "Updated description",
|
||||
Icon: "📖",
|
||||
Name: &newName,
|
||||
Description: &newDescription,
|
||||
Icon: &newIcon,
|
||||
Position: &newPos,
|
||||
Locale: "zh",
|
||||
Locale: &newLocale,
|
||||
}
|
||||
|
||||
updated, err := svc.Update(context.Background(), created.ID, updateReq)
|
||||
@@ -116,8 +120,9 @@ func TestCategoryService_Update_NotFound(t *testing.T) {
|
||||
svc, _, _ := setupCategoryService(t)
|
||||
|
||||
newPos := 1
|
||||
newName := "Nonexistent"
|
||||
updateReq := &UpdateCategoryRequest{
|
||||
Name: "Nonexistent",
|
||||
Name: &newName,
|
||||
Position: &newPos,
|
||||
}
|
||||
|
||||
@@ -248,10 +253,10 @@ func TestCategoryService_ListByPortalID(t *testing.T) {
|
||||
names := []string{"First", "Second", "Third"}
|
||||
for i, name := range names {
|
||||
_, err := svc.Create(context.Background(), portalID, accountID, &CreateCategoryRequest{
|
||||
Name: name,
|
||||
Slug: name,
|
||||
Name: name,
|
||||
Slug: name,
|
||||
Position: i + 1,
|
||||
Locale: "en",
|
||||
Locale: "en",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -307,4 +312,4 @@ func TestCategoryService_ListByPortalID_WithLocale(t *testing.T) {
|
||||
for _, cat := range categories {
|
||||
assert.Equal(t, "en", cat.Locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user