Merge pull request 'feat(category): update list filter category' (#25) from staging into main

Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
2026-08-11 15:42:49 +02:00
5 changed files with 34 additions and 0 deletions
+1
View File
@@ -30,6 +30,7 @@ type ListCategoriesRequest struct {
OrganizationID *uuid.UUID `json:"organization_id,omitempty"` OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
ParentID *uuid.UUID `json:"parent_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"` BusinessType string `json:"business_type,omitempty"`
Search string `json:"search,omitempty"` Search string `json:"search,omitempty"`
Page int `json:"page" validate:"required,min=1"` Page int `json:"page" validate:"required,min=1"`
+7
View File
@@ -196,6 +196,13 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) {
req.ParentID = &parentID 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) validationError, validationErrorCode := h.categoryValidator.ValidateListCategoriesRequest(req)
if validationError != nil { if validationError != nil {
logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed") logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed")
@@ -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 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) { func (r *CategoryRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Category, int64, error) {
var categories []*entities.Category var categories []*entities.Category
var total int64 var total int64
@@ -77,6 +92,8 @@ func (r *CategoryRepositoryImpl) List(ctx context.Context, filters map[string]in
case "outlet_id": case "outlet_id":
// Include outlet-specific categories AND global categories (outlet_id IS NULL) // Include outlet-specific categories AND global categories (outlet_id IS NULL)
query = query.Where("outlet_id = ? OR outlet_id IS NULL", value) query = query.Where("outlet_id = ? OR outlet_id IS NULL", value)
case "type":
query = applyCategoryTypeFilter(query, value)
default: default:
query = query.Where(key+" = ?", value) 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) query = query.Where("name ILIKE ? OR description ILIKE ?", searchValue, searchValue)
case "outlet_id": case "outlet_id":
query = query.Where("outlet_id = ? OR outlet_id IS NULL", value) query = query.Where("outlet_id = ? OR outlet_id IS NULL", value)
case "type":
query = applyCategoryTypeFilter(query, value)
default: default:
query = query.Where(key+" = ?", value) query = query.Where(key+" = ?", value)
} }
+3
View File
@@ -94,6 +94,9 @@ func (s *CategoryServiceImpl) ListCategories(ctx context.Context, req *contract.
if req.ParentID != nil { if req.ParentID != nil {
filters["parent_id"] = *req.ParentID filters["parent_id"] = *req.ParentID
} }
if req.Type != "" {
filters["type"] = req.Type
}
if req.Search != "" { if req.Search != "" {
filters["search"] = req.Search filters["search"] = req.Search
} }
+4
View File
@@ -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, "" return nil, ""
} }