From a7c2d6cbb3a04a9ea536f2953d1d35d57f959944 Mon Sep 17 00:00:00 2001 From: Efril Date: Thu, 6 Aug 2026 21:21:41 +0700 Subject: [PATCH] feat(category): update list filter category --- internal/contract/category_contract.go | 1 + internal/handler/category_handler.go | 7 +++++++ internal/repository/category_repository.go | 19 +++++++++++++++++++ internal/service/category_service.go | 3 +++ internal/validator/category_validator.go | 4 ++++ 5 files changed, 34 insertions(+) diff --git a/internal/contract/category_contract.go b/internal/contract/category_contract.go index 2b10a14..1950207 100644 --- a/internal/contract/category_contract.go +++ b/internal/contract/category_contract.go @@ -30,6 +30,7 @@ type ListCategoriesRequest struct { OrganizationID *uuid.UUID `json:"organization_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` ParentID *uuid.UUID `json:"parent_id,omitempty"` + Type string `json:"type,omitempty" validate:"omitempty,oneof=parent child"` BusinessType string `json:"business_type,omitempty"` Search string `json:"search,omitempty"` Page int `json:"page" validate:"required,min=1"` diff --git a/internal/handler/category_handler.go b/internal/handler/category_handler.go index a5b4e22..511873d 100644 --- a/internal/handler/category_handler.go +++ b/internal/handler/category_handler.go @@ -196,6 +196,13 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) { req.ParentID = &parentID } } + + // type=parent -> top level categories only + // type=child -> leaf categories (sub categories + top level ones without children) + if categoryType := c.Query("type"); categoryType != "" { + req.Type = categoryType + } + validationError, validationErrorCode := h.categoryValidator.ValidateListCategoriesRequest(req) if validationError != nil { logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed") diff --git a/internal/repository/category_repository.go b/internal/repository/category_repository.go index 52c9014..ee4af10 100644 --- a/internal/repository/category_repository.go +++ b/internal/repository/category_repository.go @@ -63,6 +63,21 @@ func (r *CategoryRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error return r.db.WithContext(ctx).Delete(&entities.Category{}, "id = ?", id).Error } +// applyCategoryTypeFilter narrows the query by position in the category tree. +// - "parent": top level categories only (no parent of their own) +// - "child": leaf categories — sub categories plus top level categories that +// have no sub categories, i.e. everything a product can be assigned to +func applyCategoryTypeFilter(query *gorm.DB, value interface{}) *gorm.DB { + switch value { + case "parent": + return query.Where("parent_id IS NULL") + case "child": + return query.Where("NOT EXISTS (SELECT 1 FROM categories AS sub WHERE sub.parent_id = categories.id)") + default: + return query + } +} + func (r *CategoryRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Category, int64, error) { var categories []*entities.Category var total int64 @@ -77,6 +92,8 @@ func (r *CategoryRepositoryImpl) List(ctx context.Context, filters map[string]in case "outlet_id": // Include outlet-specific categories AND global categories (outlet_id IS NULL) query = query.Where("outlet_id = ? OR outlet_id IS NULL", value) + case "type": + query = applyCategoryTypeFilter(query, value) default: query = query.Where(key+" = ?", value) } @@ -101,6 +118,8 @@ func (r *CategoryRepositoryImpl) Count(ctx context.Context, filters map[string]i query = query.Where("name ILIKE ? OR description ILIKE ?", searchValue, searchValue) case "outlet_id": query = query.Where("outlet_id = ? OR outlet_id IS NULL", value) + case "type": + query = applyCategoryTypeFilter(query, value) default: query = query.Where(key+" = ?", value) } diff --git a/internal/service/category_service.go b/internal/service/category_service.go index 21c0c00..9caee81 100644 --- a/internal/service/category_service.go +++ b/internal/service/category_service.go @@ -94,6 +94,9 @@ func (s *CategoryServiceImpl) ListCategories(ctx context.Context, req *contract. if req.ParentID != nil { filters["parent_id"] = *req.ParentID } + if req.Type != "" { + filters["type"] = req.Type + } if req.Search != "" { filters["search"] = req.Search } diff --git a/internal/validator/category_validator.go b/internal/validator/category_validator.go index 03afa9a..125ed6e 100644 --- a/internal/validator/category_validator.go +++ b/internal/validator/category_validator.go @@ -118,5 +118,9 @@ func (v *CategoryValidatorImpl) ValidateListCategoriesRequest(req *contract.List } } + if req.Type != "" && req.Type != "parent" && req.Type != "child" { + return errors.New("type must be either 'parent' or 'child'"), constants.MalformedFieldErrorCode + } + return nil, "" } -- 2.54.0