Compare commits

..
2 Commits
Author SHA1 Message Date
Efril 4f7e774043 feat(products): sort by 2026-08-11 20:47:38 +07:00
Efril a7c2d6cbb3 feat(category): update list filter category 2026-08-06 21:21:41 +07:00
6 changed files with 40 additions and 2 deletions
+1
View File
@@ -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"`
+7
View File
@@ -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")
@@ -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)
}
+6 -2
View File
@@ -112,7 +112,9 @@ func (r *ProductRepositoryImpl) List(ctx context.Context, filters map[string]int
return nil, 0, err
}
err := query.Limit(limit).Offset(offset).Find(&products).Error
// id is a tie-breaker so LIMIT/OFFSET paging stays stable when several products
// share the same created_at.
err := query.Order("created_at DESC, id DESC").Limit(limit).Offset(offset).Find(&products).Error
return products, total, err
}
@@ -265,6 +267,8 @@ func (r *ProductRepositoryImpl) ListWithOutletPrice(ctx context.Context, filters
return nil, 0, err
}
err := query.Limit(limit).Offset(offset).Find(&products).Error
// Columns are qualified because the outlet join brings a second created_at/id
// into scope. id is a tie-breaker for stable LIMIT/OFFSET paging.
err := query.Order("products.created_at DESC, products.id DESC").Limit(limit).Offset(offset).Find(&products).Error
return products, total, err
}
+3
View File
@@ -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
}
+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, ""
}