9 Commits

Author SHA1 Message Date
Daniel Goc
88255776f3 fixes 2026-04-13 14:29:36 +02:00
8024d9f739 Merge pull request 'favorites' (#57) from favorites into main
Reviewed-on: #57
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-10 08:09:13 +00:00
Daniel Goc
c5832c0cf5 minor change 2026-04-10 09:57:07 +02:00
Daniel Goc
61ccd32c4a catching errors again 2026-04-10 09:43:49 +02:00
Daniel Goc
f7f56c2928 catching errors 2026-04-10 09:33:44 +02:00
Daniel Goc
0a5ce5d9c2 ... 2026-04-10 09:13:13 +02:00
Daniel Goc
f1f5daa82b and add filtering by is_favorite 2026-04-09 14:53:56 +02:00
Daniel Goc
393de36cb2 favorites 2026-04-09 14:49:50 +02:00
9fb8e034fc Merge pull request 'added addresses endpoints' (#55) from addresses into main
Reviewed-on: #55
Reviewed-by: Wiktor Dudzic <dudzic_wiktor@ma-al.com>
2026-04-09 10:37:36 +00:00
19 changed files with 369 additions and 136 deletions

View File

@@ -34,6 +34,8 @@ func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/:id/:country_id/:quantity", handler.GetProductJson) r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
r.Get("/list", handler.ListProducts) r.Get("/list", handler.ListProducts)
r.Post("/favorite/:product_id", handler.AddToFavorites)
r.Delete("/favorite/:product_id", handler.RemoveFromFavorites)
return r return r
} }
@@ -90,7 +92,13 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
list, err := h.productService.Find(id_lang, paging, filters) userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
list, err := h.productService.Find(id_lang, userID, paging, filters)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -106,4 +114,53 @@ var columnMappingListProducts map[string]string = map[string]string{
"category_name": "cl.name", "category_name": "cl.name",
"category_id": "cp.id_category", "category_id": "cp.id_category",
"quantity": "sa.quantity", "quantity": "sa.quantity",
"is_favorite": "ps.is_favorite",
}
func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error {
productIDStr := c.Params("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.productService.AddToFavorites(userID, uint(productID))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *ProductsHandler) RemoveFromFavorites(c fiber.Ctx) error {
productIDStr := c.Params("product_id")
productID, err := strconv.Atoi(productIDStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
userID, ok := localeExtractor.GetUserID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.productService.RemoveFromFavorites(userID, uint(productID))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
} }

View File

@@ -10,7 +10,8 @@ type ScannedCategory struct {
LinkRewrite string `gorm:"column:link_rewrite"` LinkRewrite string `gorm:"column:link_rewrite"`
IsoCode string `gorm:"column:iso_code"` IsoCode string `gorm:"column:iso_code"`
Visited bool //this is for internal backend use only Visited bool // this is for internal backend use only
Filter string // filter applicable to this category
} }
type Category struct { type Category struct {
@@ -25,6 +26,7 @@ type CategoryParams struct {
CategoryID uint `json:"category_id" form:"category_id"` CategoryID uint `json:"category_id" form:"category_id"`
LinkRewrite string `json:"link_rewrite" form:"link_rewrite"` LinkRewrite string `json:"link_rewrite" form:"link_rewrite"`
Locale string `json:"locale" form:"locale"` Locale string `json:"locale" form:"locale"`
Filter string `json:"filter" form:"filter"`
} }
type CategoryInBreadcrumb struct { type CategoryInBreadcrumb struct {

View File

@@ -70,6 +70,7 @@ type ProductInList struct {
Reference string `gorm:"column:reference" json:"reference"` Reference string `gorm:"column:reference" json:"reference"`
VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"` VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"`
Quantity int64 `gorm:"column:quantity" json:"quantity"` Quantity int64 `gorm:"column:quantity" json:"quantity"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
} }
type ProductFilters struct { type ProductFilters struct {
@@ -85,3 +86,12 @@ type ProductFilters struct {
} }
type FeatVal = map[uint][]uint type FeatVal = map[uint][]uint
type B2bFavorite struct {
UserID uint `gorm:"column:user_id;not null;primaryKey" json:"user_id"`
ProductID uint `gorm:"column:product_id;not null;primaryKey" json:"product_id"`
}
func (*B2bFavorite) TableName() string {
return "b2b_favorites"
}

View File

@@ -1,48 +0,0 @@
package categoriesRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
)
type UICategoriesRepo interface {
GetAllCategories(idLang uint) ([]model.ScannedCategory, error)
}
type CategoriesRepo struct{}
func New() UICategoriesRepo {
return &CategoriesRepo{}
}
func (r *CategoriesRepo) GetAllCategories(idLang uint) ([]model.ScannedCategory, error) {
var allCategories []model.ScannedCategory
categoryTbl := (&dbmodel.PsCategory{}).TableName()
categoryLangTbl := (&dbmodel.PsCategoryLang{}).TableName()
categoryShopTbl := (&dbmodel.PsCategoryShop{}).TableName()
langTbl := (&dbmodel.PsLang{}).TableName()
err := db.Get().
Model(dbmodel.PsCategory{}).
Select(`
ps_category.id_category AS category_id,
ps_category_lang.name AS name,
ps_category.active AS active,
ps_category_shop.position AS position,
ps_category.id_parent AS id_parent,
ps_category.is_root_category AS is_root_category,
ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code
`).
Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
constdata.SHOP_ID, idLang).
Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
constdata.SHOP_ID).
Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
Scan(&allCategories).Error
return allCategories, err
}

View File

@@ -2,11 +2,14 @@ package categoryrepo
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel" "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
) )
type UICategoryRepo interface { type UICategoryRepo interface {
GetCategoryTranslations(ids []uint, idLang uint) (map[uint]string, error) GetCategoryTranslations(ids []uint, idLang uint) (map[uint]string, error)
RetrieveMenuCategories(idLang uint) ([]model.ScannedCategory, error)
} }
type CategoryRepo struct{} type CategoryRepo struct{}
@@ -42,3 +45,33 @@ func (r *CategoryRepo) GetCategoryTranslations(ids []uint, idLang uint) (map[uin
return translations, nil return translations, nil
} }
func (r *CategoryRepo) RetrieveMenuCategories(idLang uint) ([]model.ScannedCategory, error) {
var allCategories []model.ScannedCategory
categoryTbl := (&dbmodel.PsCategory{}).TableName()
categoryLangTbl := (&dbmodel.PsCategoryLang{}).TableName()
categoryShopTbl := (&dbmodel.PsCategoryShop{}).TableName()
langTbl := (&dbmodel.PsLang{}).TableName()
err := db.Get().
Model(dbmodel.PsCategory{}).
Select(`
ps_category.id_category AS category_id,
ps_category_lang.name AS name,
ps_category.active AS active,
ps_category_shop.position AS position,
ps_category.id_parent AS id_parent,
ps_category.is_root_category AS is_root_category,
ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code
`).
Joins(`LEFT JOIN `+categoryLangTbl+` ON `+categoryLangTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryLangTbl+`.id_shop = ? AND `+categoryLangTbl+`.id_lang = ?`,
constdata.SHOP_ID, idLang).
Joins(`LEFT JOIN `+categoryShopTbl+` ON `+categoryShopTbl+`.id_category = `+categoryTbl+`.id_category AND `+categoryShopTbl+`.id_shop = ?`,
constdata.SHOP_ID).
Joins(`JOIN ` + langTbl + ` ON ` + langTbl + `.id_lang = ` + categoryLangTbl + `.id_lang`).
Scan(&allCategories).Error
return allCategories, err
}

View File

@@ -16,7 +16,11 @@ import (
type UIProductsRepo interface { type UIProductsRepo interface {
GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error)
Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
AddToFavorites(userID uint, productID uint) error
RemoveFromFavorites(userID uint, productID uint) error
ExistsInFavorites(userID uint, productID uint) (bool, error)
ProductInDatabase(productID uint) (bool, error)
} }
type ProductsRepo struct{} type ProductsRepo struct{}
@@ -37,7 +41,6 @@ func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_custo
return nil, err return nil, err
} }
// Optional: validate it's valid JSON
if !json.Valid([]byte(productStr)) { if !json.Valid([]byte(productStr)) {
return nil, fmt.Errorf("invalid json returned from stored procedure") return nil, fmt.Errorf("invalid json returned from stored procedure")
} }
@@ -46,37 +49,60 @@ func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_custo
return &raw, nil return &raw, nil
} }
func (repo *ProductsRepo) Find(id_lang uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) { func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
var list []model.ProductInList var list []model.ProductInList
var total int64 var total int64
query := db.Get(). query := db.Get().
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps"). Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
Select(` Select(`
ps.id_product AS product_id, ps.id_product AS product_id,
pl.name AS name, pl.name AS name,
pl.link_rewrite AS link_rewrite, pl.link_rewrite AS link_rewrite,
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link, CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
cl.name AS category_name, cl.name AS category_name,
p.reference AS reference, p.reference AS reference,
COALESCE(v.variants_number, 0) AS variants_number, COALESCE(v.variants_number, 0) AS variants_number,
sa.quantity AS quantity sa.quantity AS quantity,
`, config.Get().Image.ImagePrefix). COALESCE(f.is_favorite, 0) AS is_favorite
`, config.Get().Image.ImagePrefix).
Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product"). Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang). Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang).
Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1"). Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1").
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang). Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang).
Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product"). Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product").
Joins("LEFT JOIN variants v ON v.id_product = ps.id_product"). Joins("LEFT JOIN variants v ON v.id_product = ps.id_product").
Joins("LEFT JOIN favorites f ON f.id_product = ps.id_product").
Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0"). Joins("LEFT JOIN ps_stock_available sa ON sa.id_product = ps.id_product AND sa.id_product_attribute = 0").
Where("ps.active = ?", 1). Where("ps.active = ?", 1).
Group("ps.id_product"). Group("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{ Clauses(exclause.With{
{ CTEs: []exclause.CTE{
Name: "variants", {
Subquery: exclause.Subquery{DB: db.Get().Model(&dbmodel.PsProductAttributeShop{}).Select("id_product", "COUNT(*) AS variants_number").Group("id_product")}, Name: "variants",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductAttributeShop{}).
Select("id_product", "COUNT(*) AS variants_number").
Group("id_product"),
},
},
{
Name: "favorites",
Subquery: exclause.Subquery{
DB: db.Get().
Table("b2b_favorites").
Select(`
product_id AS id_product,
COUNT(*) > 0 AS is_favorite
`).
Where("user_id = ?", userID).
Group("product_id"),
},
},
}, },
}}). }).
Order("ps.id_product DESC") Order("ps.id_product DESC")
// Apply all filters // Apply all filters
@@ -103,3 +129,35 @@ func (repo *ProductsRepo) Find(id_lang uint, p find.Paging, filt *filters.Filter
Count: uint(total), Count: uint(total),
}, nil }, nil
} }
func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error {
fav := model.B2bFavorite{
UserID: userID,
ProductID: productID,
}
return db.Get().Create(&fav).Error
}
func (repo *ProductsRepo) RemoveFromFavorites(userID uint, productID uint) error {
return db.Get().
Where("user_id = ? AND product_id = ?", userID, productID).
Delete(&model.B2bFavorite{}).Error
}
func (repo *ProductsRepo) ExistsInFavorites(userID uint, productID uint) (bool, error) {
var count int64
err := db.Get().
Table("b2b_favorites").
Where("user_id = ? AND product_id = ?", userID, productID).
Count(&count).Error
return count >= 1, err
}
func (repo *ProductsRepo) ProductInDatabase(productID uint) (bool, error) {
var count int64
err := db.Get().
Table(dbmodel.TableNamePsProduct).
Where(dbmodel.PsProductCols.IDProduct.Col()+" = ?", productID).
Count(&count).Error
return count >= 1, err
}

View File

@@ -3,31 +3,45 @@ package menuService
import ( import (
"slices" "slices"
"sort" "sort"
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/categoriesRepo" categoryrepo "git.ma-al.com/goc_daniel/b2b/app/repos/categoryRepo"
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo" routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
) )
type MenuService struct { type MenuService struct {
categoriesRepo categoriesRepo.UICategoriesRepo categoryRepo categoryrepo.UICategoryRepo
routesRepo routesRepo.UIRoutesRepo routesRepo routesRepo.UIRoutesRepo
} }
func New() *MenuService { func New() *MenuService {
return &MenuService{ return &MenuService{
categoriesRepo: categoriesRepo.New(), categoryRepo: categoryrepo.New(),
routesRepo: routesRepo.New(), routesRepo: routesRepo.New(),
} }
} }
func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) { func (s *MenuService) GetCategoryTree(root_category_id uint, id_lang uint) (*model.Category, error) {
all_categories, err := s.categoriesRepo.GetAllCategories(id_lang) all_categories, err := s.categoryRepo.RetrieveMenuCategories(id_lang)
if err != nil { if err != nil {
return &model.Category{}, err return &model.Category{}, err
} }
// remove blacklisted categories
// to do so, we detach them from the main tree
for i := 0; i < len(all_categories); i++ {
if slices.Contains(constdata.CATEGORY_BLACKLIST, all_categories[i].CategoryID) {
all_categories[i].ParentID = all_categories[i].CategoryID
}
}
iso_code := all_categories[0].IsoCode
s.appendAdditional(&all_categories, id_lang, iso_code)
// find the root // find the root
root_index := 0 root_index := 0
root_found := false root_found := false
@@ -98,7 +112,7 @@ func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) mod
normal.CategoryID = scanned.CategoryID normal.CategoryID = scanned.CategoryID
normal.Label = scanned.Name normal.Label = scanned.Name
// normal.Active = scanned.Active == 1 // normal.Active = scanned.Active == 1
normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode} normal.Params = model.CategoryParams{CategoryID: normal.CategoryID, LinkRewrite: scanned.LinkRewrite, Locale: scanned.IsoCode, Filter: scanned.Filter}
normal.Children = []model.Category{} normal.Children = []model.Category{}
return normal return normal
} }
@@ -114,11 +128,14 @@ 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 (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) { 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) all_categories, err := s.categoryRepo.RetrieveMenuCategories(id_lang)
if err != nil { if err != nil {
return []model.CategoryInBreadcrumb{}, err return []model.CategoryInBreadcrumb{}, err
} }
iso_code := all_categories[0].IsoCode
s.appendAdditional(&all_categories, id_lang, iso_code)
breadcrumb := []model.CategoryInBreadcrumb{} breadcrumb := []model.CategoryInBreadcrumb{}
start_index := 0 start_index := 0
@@ -211,3 +228,24 @@ func (s *MenuService) GetTopMenu(languageId uint, roleId uint) ([]*model.B2BTopM
return roots, nil return roots, nil
} }
func (s *MenuService) appendAdditional(all_categories *[]model.ScannedCategory, id_lang uint, iso_code string) {
for i := 0; i < len(*all_categories); i++ {
(*all_categories)[i].Filter = "category_id_in=" + strconv.Itoa(int((*all_categories)[i].CategoryID))
}
var additional model.ScannedCategory
additional.CategoryID = 10001
additional.Name = "New Products"
additional.Active = 1
additional.Position = 10
additional.ParentID = 2
additional.IsRoot = 0
additional.LinkRewrite = i18n.T___(id_lang, "category.new_products")
additional.IsoCode = iso_code
additional.Visited = false
additional.Filter = "is_new_in=true"
*all_categories = append(*all_categories, additional)
}

View File

@@ -8,6 +8,7 @@ import (
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
) )
type ProductService struct { type ProductService struct {
@@ -29,6 +30,46 @@ func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_
return products, nil return products, nil
} }
func (s *ProductService) Find(id_lang uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) { func (s *ProductService) Find(id_lang, userID uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
return s.productsRepo.Find(id_lang, p, filters) return s.productsRepo.Find(id_lang, userID, p, filters)
}
func (s *ProductService) AddToFavorites(userID uint, productID uint) error {
exists, err := s.productsRepo.ProductInDatabase(productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrProductNotFound
}
exists, err = s.productsRepo.ExistsInFavorites(userID, productID)
if err != nil {
return err
}
if exists {
return responseErrors.ErrAlreadyInFavorites
}
return s.productsRepo.AddToFavorites(userID, productID)
}
func (s *ProductService) RemoveFromFavorites(userID uint, productID uint) error {
exists, err := s.productsRepo.ProductInDatabase(productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrProductNotFound
}
exists, err = s.productsRepo.ExistsInFavorites(userID, productID)
if err != nil {
return err
}
if !exists {
return responseErrors.ErrNotInFavorites
}
return s.productsRepo.RemoveFromFavorites(userID, productID)
} }

View File

@@ -4,6 +4,7 @@ import (
"strings" "strings"
"unicode" "unicode"
"git.ma-al.com/goc_daniel/b2b/app/db"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"golang.org/x/text/runes" "golang.org/x/text/runes"
@@ -22,7 +23,7 @@ func SanitizeSlug(s string) string {
s = strings.TrimSpace(strings.ToLower(s)) s = strings.TrimSpace(strings.ToLower(s))
// First apply explicit transliteration for language-specific letters. // First apply explicit transliteration for language-specific letters.
s = transliterateWithTable(s) s = transliterateSlug(s)
// Then normalize and strip any remaining combining marks. // Then normalize and strip any remaining combining marks.
s = removeDiacritics(s) s = removeDiacritics(s)
@@ -40,19 +41,17 @@ func SanitizeSlug(s string) string {
return s return s
} }
func transliterateWithTable(s string) string { func transliterateSlug(s string) string {
var b strings.Builder var cleared string
b.Grow(len(s))
for _, r := range s { err := db.DB.Raw("SELECT slugify_eu(?)", s).Scan(&cleared).Error
if repl, ok := constdata.TRANSLITERATION_TABLE[r]; ok { if err != nil {
b.WriteString(repl) // log error
} else { _ = err
b.WriteRune(r) return s
}
} }
return b.String() return cleared
} }
func removeDiacritics(s string) string { func removeDiacritics(s string) string {

View File

@@ -9,6 +9,9 @@ const ADMIN_NOTIFICATION_LANGUAGE = 2
// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=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 CATEGORY_TREE_ROOT_ID = 2
// since arrays can not be const
var CATEGORY_BLACKLIST = []uint{250}
const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const MAX_AMOUNT_OF_CARTS_PER_USER = 10
const DEFAULT_NEW_CART_NAME = "new cart" const DEFAULT_NEW_CART_NAME = "new cart"
@@ -25,23 +28,3 @@ const WEBDAV_TRIMMED_ROOT = "localhost:3000/api/v1/webdav/storage"
const NON_ALNUM_REGEX = `[^a-z0-9]+` const NON_ALNUM_REGEX = `[^a-z0-9]+`
const MULTI_DASH_REGEX = `-+` const MULTI_DASH_REGEX = `-+`
const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$` const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$`
// Currently supports only German+Polish specific cases
var TRANSLITERATION_TABLE = map[rune]string{
// German
'ä': "ae",
'ö': "oe",
'ü': "ue",
'ß': "ss",
// Polish
'ą': "a",
'ć': "c",
'ę': "e",
'ł': "l",
'ń': "n",
'ó': "o",
'ś': "s",
'ż': "z",
'ź': "z",
}

View File

@@ -49,8 +49,11 @@ var (
ErrAIResponseFail = errors.New("AI responded with failure") ErrAIResponseFail = errors.New("AI responded with failure")
ErrAIBadOutput = errors.New("AI response does not obey the format") ErrAIBadOutput = errors.New("AI response does not obey the format")
// Typed errors for product list handler // Typed errors for product handler
ErrBadPaging = errors.New("bad or missing paging attribute value in header") ErrBadPaging = errors.New("bad or missing paging attribute value in header")
ErrProductNotFound = errors.New("product with provided id does not exist")
ErrAlreadyInFavorites = errors.New("the product already is in your favorites")
ErrNotInFavorites = errors.New("the product already is not in your favorites")
// Typed errors for menu handler // Typed errors for menu handler
ErrNoRootFound = errors.New("no root found in categories table") ErrNoRootFound = errors.New("no root found in categories table")
@@ -170,6 +173,12 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrBadPaging): case errors.Is(err, ErrBadPaging):
return i18n.T_(c, "error.err_bad_paging") return i18n.T_(c, "error.err_bad_paging")
case errors.Is(err, ErrProductNotFound):
return i18n.T_(c, "error.err_product_not_found")
case errors.Is(err, ErrAlreadyInFavorites):
return i18n.T_(c, "error.err_already_in_favorites")
case errors.Is(err, ErrNotInFavorites):
return i18n.T_(c, "error.err_already_not_in_favorites")
case errors.Is(err, ErrNoRootFound): case errors.Is(err, ErrNoRootFound):
return i18n.T_(c, "error.err_no_root_found") return i18n.T_(c, "error.err_no_root_found")
@@ -249,6 +258,9 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidURLSlug), errors.Is(err, ErrInvalidURLSlug),
errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrInvalidXHTML),
errors.Is(err, ErrBadPaging), errors.Is(err, ErrBadPaging),
errors.Is(err, ErrProductNotFound),
errors.Is(err, ErrAlreadyInFavorites),
errors.Is(err, ErrNotInFavorites),
errors.Is(err, ErrNoRootFound), errors.Is(err, ErrNoRootFound),
errors.Is(err, ErrCircularDependency), errors.Is(err, ErrCircularDependency),
errors.Is(err, ErrStartCategoryNotFound), errors.Is(err, ErrStartCategoryNotFound),

View File

@@ -0,0 +1,15 @@
info:
name: Add To Favorites
type: http
seq: 3
http:
method: POST
url: "{{bas_url}}/restricted/product/favorite/53"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,15 @@
info:
name: Remove Form Favorites
type: http
seq: 4
http:
method: DELETE
url: "{{bas_url}}/restricted/product/favorite/51"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=10&category_id=13 url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=2&category_id=13
params: params:
- name: root_category_id - name: root_category_id
value: "10" value: "2"
type: query type: query
- name: category_id - name: category_id
value: "13" value: "13"

View File

@@ -5,10 +5,10 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=10 url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=2
params: params:
- name: root_category_id - name: root_category_id
value: "10" value: "2"
type: query type: query
auth: inherit auth: inherit

6
go.mod
View File

@@ -36,8 +36,6 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
@@ -100,10 +98,10 @@ require (
github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xyproto/randomstring v1.2.0 // indirect github.com/xyproto/randomstring v1.2.0 // indirect
golang.org/x/net v0.52.0 golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect golang.org/x/text v0.35.0
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
) )

6
go.sum
View File

@@ -72,8 +72,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg= github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
@@ -136,8 +134,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
@@ -158,8 +154,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

View File

@@ -151,6 +151,16 @@ CREATE TABLE IF NOT EXISTS b2b_carts_products (
CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (cart_id); CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (cart_id);
-- favorites
CREATE TABLE IF NOT EXISTS b2b_favorites (
user_id BIGINT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL,
PRIMARY KEY (user_id, product_id),
CONSTRAINT fk_favorites_customer FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_favorites_product FOREIGN KEY (product_id) REFERENCES ps_product(id_product) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-- refresh_tokens -- refresh_tokens
CREATE TABLE IF NOT EXISTS b2b_refresh_tokens ( CREATE TABLE IF NOT EXISTS b2b_refresh_tokens (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
@@ -395,16 +405,26 @@ DELIMITER ;
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS b2b_countries; DROP TABLE IF EXISTS b2b_addresses;
DROP TABLE IF EXISTS b2b_language; DROP TABLE IF EXISTS b2b_top_menu_roles;
DROP TABLE IF EXISTS b2b_components; DROP TABLE IF EXISTS b2b_favorites;
DROP TABLE IF EXISTS b2b_scopes; DROP TABLE IF EXISTS b2b_carts_products;
DROP TABLE IF EXISTS b2b_translations; DROP TABLE IF EXISTS b2b_customer_carts;
DROP TABLE IF EXISTS b2b_customers; DROP TABLE IF EXISTS b2b_specific_price_country;
DROP TABLE IF EXISTS b2b_refresh_tokens; DROP TABLE IF EXISTS b2b_specific_price_customer;
DROP TABLE IF EXISTS b2b_currencies;
DROP TABLE IF EXISTS b2b_currency_rates;
DROP TABLE IF EXISTS b2b_specific_price;
DROP TABLE IF EXISTS b2b_specific_price_product;
DROP TABLE IF EXISTS b2b_specific_price_category;
DROP TABLE IF EXISTS b2b_specific_price_product_attribute; DROP TABLE IF EXISTS b2b_specific_price_product_attribute;
DROP TABLE IF EXISTS b2b_specific_price_category;
DROP TABLE IF EXISTS b2b_specific_price_product;
DROP TABLE IF EXISTS b2b_specific_price;
DROP TABLE IF EXISTS b2b_role_permissions;
DROP TABLE IF EXISTS b2b_permissions;
DROP TABLE IF EXISTS b2b_roles;
DROP TABLE IF EXISTS b2b_countries;
DROP TABLE IF EXISTS b2b_currency_rates;
DROP TABLE IF EXISTS b2b_currencies;
DROP TABLE IF EXISTS b2b_refresh_tokens;
DROP TABLE IF EXISTS b2b_customers;
DROP TABLE IF EXISTS b2b_translations;
DROP TABLE IF EXISTS b2b_scopes;
DROP TABLE IF EXISTS b2b_components;
DROP TABLE IF EXISTS b2b_language;

View File

@@ -132,6 +132,12 @@ JSON_OBJECT(
m.name, m.name,
'category', 'category',
cl.name, cl.name,
/* ================= FAVORITE ================= */
'is_favorite',
EXISTS(
SELECT 1 FROM b2b_favorites f
WHERE f.user_id = p_id_customer AND f.product_id = p_id_product
),
/* ================= IMAGE ================= */ /* ================= IMAGE ================= */
'cover_image', 'cover_image',
JSON_OBJECT( JSON_OBJECT(