From fa89723eb61ee31012f40003f11520e61617f2c5 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 31 Mar 2026 11:40:57 +0200 Subject: [PATCH] add get-breadcrumb endpoint --- app/delivery/web/api/restricted/menu.go | 31 +++++++++++ app/delivery/web/api/settings.go | 20 ++++--- app/model/category.go | 33 +++++++++++ app/model/product.go | 26 --------- app/service/menuService/menuService.go | 64 ++++++++++++++++++++++ app/utils/const_data/consts.go | 5 ++ app/utils/responseErrors/responseErrors.go | 12 +++- bruno/b2b-daniel/get-breadcrumb.yml | 22 ++++++++ bruno/b2b-daniel/get-category-tree.yml | 4 +- 9 files changed, 179 insertions(+), 38 deletions(-) create mode 100644 app/model/category.go create mode 100644 bruno/b2b-daniel/get-breadcrumb.yml diff --git a/app/delivery/web/api/restricted/menu.go b/app/delivery/web/api/restricted/menu.go index cee5673..b269fb7 100644 --- a/app/delivery/web/api/restricted/menu.go +++ b/app/delivery/web/api/restricted/menu.go @@ -26,6 +26,7 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router { handler := NewMenuHandler() r.Get("/get-category-tree", handler.GetCategoryTree) + r.Get("/get-breadcrumb", handler.GetBreadcrumb) r.Get("/get-top-menu", handler.GetTopMenu) return r @@ -54,6 +55,36 @@ func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error { return c.JSON(response.Make(&category_tree, 0, i18n.T_(c, response.Message_OK))) } +func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error { + lang_id, ok := c.Locals("langID").(uint) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + root_category_id_attribute := c.Query("root_category_id") + root_category_id, err := strconv.Atoi(root_category_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + category_id_attribute := c.Query("category_id") + category_id, err := strconv.Atoi(category_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + breadcrumb, err := h.menuService.GetBreadcrumb(uint(root_category_id), uint(category_id), lang_id) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&breadcrumb, 0, i18n.T_(c, response.Message_OK))) +} + func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error { lang_id, ok := c.Locals("langID").(uint) if !ok { diff --git a/app/delivery/web/api/settings.go b/app/delivery/web/api/settings.go index eaa4acc..fb7af6c 100644 --- a/app/delivery/web/api/settings.go +++ b/app/delivery/web/api/settings.go @@ -21,10 +21,12 @@ type SettingsResponse struct { // AppSettings represents app configuration type AppSettings struct { - Name string `json:"name"` - Environment string `json:"environment"` - BaseURL string `json:"base_url"` - PasswordRegex string `json:"password_regex"` + Name string `json:"name"` + Environment string `json:"environment"` + BaseURL string `json:"base_url"` + PasswordRegex string `json:"password_regex"` + CategoryTreeRootID uint `json:"category_tree_root_id"` + ShopDefaultLanguage uint `json:"shop_default_language"` // Config config.Config `json:"config"` } @@ -65,10 +67,12 @@ func (h *SettingsHandler) GetSettings(cfg *config.Config) fiber.Handler { return func(c fiber.Ctx) error { settings := SettingsResponse{ App: AppSettings{ - Name: cfg.App.Name, - Environment: cfg.App.Environment, - BaseURL: cfg.App.BaseURL, - PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX, + Name: cfg.App.Name, + Environment: cfg.App.Environment, + BaseURL: cfg.App.BaseURL, + PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX, + CategoryTreeRootID: constdata.CATEGORY_TREE_ROOT_ID, + ShopDefaultLanguage: constdata.SHOP_DEFAULT_LANGUAGE, // Config: *config.Get(), }, Server: ServerSettings{ diff --git a/app/model/category.go b/app/model/category.go new file mode 100644 index 0000000..50e6ce9 --- /dev/null +++ b/app/model/category.go @@ -0,0 +1,33 @@ +package model + +type ScannedCategory struct { + CategoryID uint `gorm:"column:category_id;primaryKey"` + Name string `gorm:"column:name"` + Active uint `gorm:"column:active"` + Position uint `gorm:"column:position"` + ParentID uint `gorm:"column:id_parent"` + IsRoot uint `gorm:"column:is_root_category"` + LinkRewrite string `gorm:"column:link_rewrite"` + IsoCode string `gorm:"column:iso_code"` + + Visited bool //this is for internal backend use only +} + +type Category struct { + CategoryID uint `json:"category_id" form:"category_id"` + Label string `json:"label" form:"label"` + // Active bool `json:"active" form:"active"` + Params CategoryParams `json:"params" form:"params"` + Children []Category `json:"children" form:"children"` +} + +type CategoryParams struct { + CategoryID uint `json:"category_id" form:"category_id"` + LinkRewrite string `json:"link_rewrite" form:"link_rewrite"` + Locale string `json:"locale" form:"locale"` +} + +type CategoryInBreadcrumb struct { + CategoryID uint `json:"category_id" form:"category_id"` + Name string `json:"name" form:"name"` +} diff --git a/app/model/product.go b/app/model/product.go index 51a646b..fa47790 100644 --- a/app/model/product.go +++ b/app/model/product.go @@ -84,30 +84,4 @@ type ProductFilters struct { InStock uint `query:"stock,omitempty"` } -type ScannedCategory struct { - CategoryID uint `gorm:"column:category_id;primaryKey"` - Name string `gorm:"column:name"` - Active uint `gorm:"column:active"` - Position uint `gorm:"column:position"` - ParentID uint `gorm:"column:id_parent"` - IsRoot uint `gorm:"column:is_root_category"` - LinkRewrite string `gorm:"column:link_rewrite"` - IsoCode string `gorm:"column:iso_code"` - - Visited bool //this is for internal backend use only -} -type Category struct { - CategoryID uint `json:"category_id" form:"category_id"` - Label string `json:"label" form:"label"` - // Active bool `json:"active" form:"active"` - Params CategoryParams `json:"params" form:"params"` - Children []Category `json:"children" form:"children"` -} - -type CategoryParams struct { - CategoryID uint `json:"category_id" form:"category_id"` - LinkRewrite string `json:"link_rewrite" form:"link_rewrite"` - Locale string `json:"locale" form:"locale"` -} - type FeatVal = map[uint][]uint diff --git a/app/service/menuService/menuService.go b/app/service/menuService/menuService.go index 4e4b100..2d72cea 100644 --- a/app/service/menuService/menuService.go +++ b/app/service/menuService/menuService.go @@ -1,6 +1,7 @@ package menuService import ( + "slices" "sort" "git.ma-al.com/goc_daniel/b2b/app/model" @@ -112,6 +113,69 @@ func (a ByPosition) Len() int { return len(a) } func (a ByPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByPosition) Less(i, j int) bool { return a[i].Position < a[j].Position } +func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uint, id_lang uint) ([]model.CategoryInBreadcrumb, error) { + all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) + if err != nil { + return []model.CategoryInBreadcrumb{}, err + } + + breadcrumb := []model.CategoryInBreadcrumb{} + + start_index := 0 + start_found := false + for i := 0; i < len(all_categories); i++ { + if all_categories[i].CategoryID == start_category_id { + start_index = i + start_found = true + break + } + } + if !start_found { + return []model.CategoryInBreadcrumb{}, responseErrors.ErrStartCategoryNotFound + } + + // map category ids to indices + id_to_index := make(map[uint]int) + for i := 0; i < len(all_categories); i++ { + all_categories[i].Visited = false + id_to_index[all_categories[i].CategoryID] = i + } + + // do a simple graph traversal, always jumping from node to its parent + index := start_index + success := true + for { + if all_categories[index].Visited { + success = false + break + } + all_categories[index].Visited = true + + var next_category model.CategoryInBreadcrumb + next_category.CategoryID = all_categories[index].CategoryID + next_category.Name = all_categories[index].Name + breadcrumb = append(breadcrumb, next_category) + + if all_categories[index].CategoryID == root_category_id { + break + } + next_index, ok := id_to_index[all_categories[index].ParentID] + if !ok { + success = false + break + } + index = next_index + } + + slices.Reverse(breadcrumb) + + if !success { + return breadcrumb, responseErrors.ErrRootNeverReached + } + + return breadcrumb, nil +} + func (s *MenuService) GetTopMenu(id uint) ([]*model.B2BTopMenu, error) { items, err := s.routesRepo.GetTopMenu(id) if err != nil { diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index 812364f..9c64ee5 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -3,6 +3,11 @@ package constdata // PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads). const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` const SHOP_ID = 1 +const SHOP_DEFAULT_LANGUAGE = 1 + +// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1 +const CATEGORY_TREE_ROOT_ID = 2 + const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const DEFAULT_NEW_CART_NAME = "new cart" diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index 2656a3b..c4247ea 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -50,8 +50,10 @@ var ( ErrBadPaging = errors.New("bad or missing paging attribute value in header") // Typed errors for menu handler - ErrNoRootFound = errors.New("no root found in categories table") - ErrCircularDependency = errors.New("circular dependency structure in tree (could be caused by improper root id)") + ErrNoRootFound = errors.New("no root found in categories table") + ErrCircularDependency = errors.New("circular dependency structure in tree (could be caused by improper root id)") + ErrStartCategoryNotFound = errors.New("the start category has not been found") + ErrRootNeverReached = errors.New("the root category is not an ancestor of start category") // Typed errors for carts handler ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") @@ -148,6 +150,10 @@ func GetErrorCode(c fiber.Ctx, err error) string { return i18n.T_(c, "error.no_root_found") case errors.Is(err, ErrCircularDependency): return i18n.T_(c, "error.circular_dependency") + case errors.Is(err, ErrStartCategoryNotFound): + return i18n.T_(c, "error.start_category_not_found") + case errors.Is(err, ErrRootNeverReached): + return i18n.T_(c, "error.root_never_reached") case errors.Is(err, ErrMaxAmtOfCartsReached): return i18n.T_(c, "error.max_amt_of_carts_reached") @@ -193,6 +199,8 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrBadPaging), errors.Is(err, ErrNoRootFound), errors.Is(err, ErrCircularDependency), + errors.Is(err, ErrStartCategoryNotFound), + errors.Is(err, ErrRootNeverReached), errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrProductOrItsVariationDoesNotExist): diff --git a/bruno/b2b-daniel/get-breadcrumb.yml b/bruno/b2b-daniel/get-breadcrumb.yml new file mode 100644 index 0000000..8b10c00 --- /dev/null +++ b/bruno/b2b-daniel/get-breadcrumb.yml @@ -0,0 +1,22 @@ +info: + name: get-breadcrumb + type: http + seq: 18 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=10&category_id=13 + params: + - name: root_category_id + value: "10" + type: query + - name: category_id + value: "13" + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/get-category-tree.yml b/bruno/b2b-daniel/get-category-tree.yml index 6073e4f..c6b436e 100644 --- a/bruno/b2b-daniel/get-category-tree.yml +++ b/bruno/b2b-daniel/get-category-tree.yml @@ -5,10 +5,10 @@ info: http: method: GET - url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=3 + url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=10 params: - name: root_category_id - value: "3" + value: "10" type: query auth: inherit