8 Commits

Author SHA1 Message Date
Daniel Goc
0cb5cc47bb Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into update_categories 2026-04-14 12:33:11 +02:00
Daniel Goc
1efc5417be permissions strings change 2026-04-14 12:32:24 +02:00
Daniel Goc
a0c3dd8ec8 added filtering on is_new and is_favorite 2026-04-14 12:28:39 +02:00
26cbdeec0a Merge pull request 'product-procedures' (#59) from product-procedures into main
Reviewed-on: #59
Reviewed-by: goc_daniel <goc_daniel@ma-al.com>
2026-04-13 13:32:28 +00:00
Daniel Goc
ce4cadaa16 most importantly: new category and filter on is_new 2026-04-13 15:29:21 +02:00
Daniel Goc
7f05d39b38 Merge branch 'product-procedures' of ssh://git.ma-al.com:8822/goc_daniel/b2b into update_categories 2026-04-13 14:48:28 +02:00
Daniel Goc
88255776f3 fixes 2026-04-13 14:29:36 +02:00
38cb07f3d4 chore: address pull request review issues 2026-04-13 14:22:14 +02:00
30 changed files with 427 additions and 516 deletions

View File

@@ -10,7 +10,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/authService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"github.com/gofiber/fiber/v3"
)
@@ -115,26 +114,6 @@ func AuthMiddleware() fiber.Handler {
}
}
// RequireAdmin creates admin-only middleware
func RequireAdmin() fiber.Handler {
return func(c fiber.Ctx) error {
originalUserRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "not authenticated",
})
}
if model.CustomerRole(originalUserRole.Name) != model.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "admin access required",
})
}
return c.Next()
}
}
// Webdav
func Webdav() fiber.Handler {
authService := authService.NewAuthService()

View File

@@ -2,27 +2,27 @@ package middleware
import (
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
func Require(p perms.Permission) fiber.Handler {
return func(c fiber.Ctx) error {
u := c.Locals("user")
if u == nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
user, ok := u.(*model.UserSession)
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.SendStatus(fiber.StatusInternalServerError)
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
for _, perm := range user.Permissions {
if perm == p {
for _, perm := range user.Role.Permissions {
if perm.Name == p {
return c.Next()
}
}
return c.SendStatus(fiber.StatusForbidden)
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrForbidden)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrForbidden)))
}
}

View File

@@ -3,8 +3,13 @@ package perms
type Permission string
const (
UserReadAny Permission = "user.read.any"
UserWriteAny Permission = "user.write.any"
UserDeleteAny Permission = "user.delete.any"
CurrencyWrite Permission = "currency.write"
UserReadAny Permission = "user.read.any"
UserWriteAny Permission = "user.write.any"
UserDeleteAny Permission = "user.delete.any"
CurrencyWrite Permission = "currency.write"
SpecificPriceManage Permission = "specific_price.manage"
WebdavCreateToken Permission = "webdav.create_token"
ProductTranslationSave Permission = "product_translation.save"
ProductTranslationTranslate Permission = "product_translation.translate"
SearchCreateIndex Permission = "search.create_index"
)

View File

@@ -4,7 +4,7 @@ import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"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/service/productService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
@@ -82,7 +82,7 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
}
func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingListProducts)
paging, filters, err := query_params.ParseFilters[dbmodel.PsProduct](c, columnMappingListProducts)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -94,7 +94,7 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, 1, constdata.SHOP_ID)
list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, constdata.DEFAULT_PRODUCT_QUANTITY, constdata.SHOP_ID)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
@@ -103,14 +103,15 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK)))
}
// These are all the filterable fields
var columnMappingListProducts map[string]string = map[string]string{
"product_id": "ps.id_product",
"name": "pl.name",
"reference": "p.reference",
"category_name": "cl.name",
"category_id": "cp.id_category",
"quantity": "sa.quantity",
"is_favorite": "ps.is_favorite",
"product_id": "bp.product_id",
"name": "bp.name",
"reference": "bp.reference",
"category_id": "bp.category_id",
"quantity": "bp.quantity",
"is_favorite": "bp.is_favorite",
"is_new": "bp.is_new",
}
func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error {
@@ -176,7 +177,7 @@ func (h *ProductsHandler) ListProductVariants(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
list, err := h.productService.GetProductAttributes(customer.LangID, uint(productID), constdata.SHOP_ID, customer.ID, customer.CountryID, 1)
list, err := h.productService.GetProductAttributes(customer.LangID, uint(productID), constdata.SHOP_ID, customer.ID, customer.CountryID, constdata.DEFAULT_PRODUCT_QUANTITY)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -4,7 +4,8 @@ import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
@@ -35,8 +36,8 @@ func ProductTranslationHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewProductTranslationHandler()
r.Get("/get-product-description", handler.GetProductDescription)
r.Post("/save-product-description", handler.SaveProductDescription)
r.Get("/translate-product-description", handler.TranslateProductDescription)
r.Post("/save-product-description", middleware.Require(perms.ProductTranslationSave), handler.SaveProductDescription)
r.Get("/translate-product-description", middleware.Require(perms.ProductTranslationTranslate), handler.TranslateProductDescription)
return r
}
@@ -80,12 +81,6 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute)
if err != nil {
@@ -123,12 +118,6 @@ func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) err
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
productID_attribute := c.Query("productID")
productID, err := strconv.Atoi(productID_attribute)
if err != nil {

View File

@@ -4,7 +4,8 @@ import (
"encoding/json"
"fmt"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService"
searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
@@ -30,7 +31,7 @@ func NewMeiliSearchHandler() *MeiliSearchHandler {
func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMeiliSearchHandler()
r.Get("/create-index", handler.CreateIndex)
r.Get("/create-index", middleware.Require(perms.SearchCreateIndex), handler.CreateIndex)
r.Post("/search", handler.Search)
r.Post("/settings", handler.GetSettings)
@@ -44,12 +45,6 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
err := h.meiliService.CreateIndex(id_lang)
if err != nil {
fmt.Printf("CreateIndex error: %v\n", err)

View File

@@ -2,11 +2,15 @@ package restricted
import (
"strconv"
"time"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/specificPriceService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
@@ -27,13 +31,13 @@ func NewSpecificPriceHandler() *SpecificPriceHandler {
func SpecificPriceHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewSpecificPriceHandler()
r.Post("/", handler.Create)
r.Put("/:id", handler.Update)
r.Delete("/:id", handler.Delete)
r.Get("/", handler.List)
r.Get("/:id", handler.GetByID)
r.Patch("/:id/activate", handler.Activate)
r.Patch("/:id/deactivate", handler.Deactivate)
r.Post("/", middleware.Require(perms.SpecificPriceManage), handler.Create)
r.Put("/:id", middleware.Require(perms.SpecificPriceManage), handler.Update)
r.Delete("/:id", middleware.Require(perms.SpecificPriceManage), handler.Delete)
r.Get("/", middleware.Require(perms.SpecificPriceManage), handler.List)
r.Get("/:id", middleware.Require(perms.SpecificPriceManage), handler.GetByID)
r.Patch("/:id/activate", middleware.Require(perms.SpecificPriceManage), handler.Activate)
r.Patch("/:id/deactivate", middleware.Require(perms.SpecificPriceManage), handler.Deactivate)
return r
}
@@ -41,147 +45,116 @@ func SpecificPriceHandlerRoutes(r fiber.Router) fiber.Router {
func (h *SpecificPriceHandler) Create(c fiber.Ctx) error {
var pr model.SpecificPrice
if err := c.Bind().Body(&pr); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
})
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
result, err := h.SpecificPriceService.Create(c.Context(), &pr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.Status(fiber.StatusCreated).JSON(result)
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Update(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
})
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
var pr model.SpecificPrice
if err := c.Bind().Body(&pr); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
})
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
result, err := h.SpecificPriceService.Update(c.Context(), id, &pr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(result)
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) List(c fiber.Ctx) error {
result, err := h.SpecificPriceService.List(c.Context())
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(result)
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) GetByID(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
})
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
result, err := h.SpecificPriceService.GetByID(c.Context(), id)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(result)
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Activate(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
})
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.SetActive(c.Context(), id, true)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(fiber.Map{
"message": "price reduction activated",
})
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Deactivate(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
})
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.SetActive(c.Context(), id, false)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(fiber.Map{
"message": "price reduction deactivated",
})
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}
func (h *SpecificPriceHandler) Delete(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
})
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
err = h.SpecificPriceService.Delete(c.Context(), id)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(fiber.Map{
"message": "specific price deleted",
})
}
func parseTime(s *string) *time.Time {
if s == nil {
return nil
}
t, err := time.Parse(time.RFC3339, *s)
if err != nil {
return nil
}
return &t
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -4,7 +4,8 @@ import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/storageService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
@@ -34,7 +35,7 @@ func StorageHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/download-file/*", handler.DownloadFile)
// for admins only
r.Get("/create-new-webdav-token", handler.CreateNewWebdavToken)
r.Get("/create-new-webdav-token", middleware.Require(perms.WebdavCreateToken), handler.CreateNewWebdavToken)
return r
}
@@ -84,12 +85,6 @@ func (h *StorageHandler) CreateNewWebdavToken(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
userRole, ok := localeExtractor.GetOriginalUserRole(c)
if !ok || model.CustomerRole(userRole.Name) != model.RoleAdmin {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrAdminAccessRequired)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrAdminAccessRequired)))
}
new_token, err := h.storageService.NewWebdavToken(userID)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).

View File

@@ -161,16 +161,6 @@ func (s *Server) Setup() error {
// })
// })
// // Admin routes example
// admin := s.api.Group("/admin")
// admin.Use(middleware.AuthMiddleware())
// admin.Use(middleware.RequireAdmin())
// admin.Get("/users", func(c fiber.Ctx) error {
// return c.JSON(fiber.Map{
// "message": "Admin area - user management",
// })
// })
// keep this at the end because its wilderange
general.InitBo(s.App())

View File

@@ -10,7 +10,8 @@ type ScannedCategory struct {
LinkRewrite string `gorm:"column:link_rewrite"`
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 {
@@ -25,6 +26,7 @@ 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"`
Filter string `json:"filter" form:"filter"`
}
type CategoryInBreadcrumb struct {

View File

@@ -1,66 +1,5 @@
package model
// Product contains each and every column from the table ps_product.
type Product struct {
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
SupplierID uint `gorm:"column:id_supplier" json:"supplier_id" form:"supplier_id"`
ManufacturerID uint `gorm:"column:id_manufacturer" json:"manufacturer_id" form:"manufacturer_id"`
CategoryDefaultID uint `gorm:"column:id_category_default" json:"category_default_id" form:"category_default_id"`
ShopDefaultID uint `gorm:"column:id_shop_default" json:"shop_default_id" form:"shop_default_id"`
TaxRulesGroupID uint `gorm:"column:id_tax_rules_group" json:"tax_rules_group_id" form:"tax_rules_group_id"`
OnSale uint `gorm:"column:on_sale" json:"on_sale" form:"on_sale"`
OnlineOnly uint `gorm:"column:online_only" json:"online_only" form:"online_only"`
EAN13 string `gorm:"column:ean13;type:varchar(13)" json:"ean13" form:"ean13"`
ISBN string `gorm:"column:isbn;type:varchar(32)" json:"isbn" form:"isbn"`
UPC string `gorm:"column:upc;type:varchar(12)" json:"upc" form:"upc"`
EkoTax float32 `gorm:"column:eko_tax;type:decimal(20,6)" json:"eko_tax" form:"eko_tax"`
Quantity uint `gorm:"column:quantity" json:"quantity" form:"quantity"`
MinimalQuantity uint `gorm:"column:minimal_quantity" json:"minimal_quantity" form:"minimal_quantity"`
LowStockThreshold uint `gorm:"column:low_stock_threshold" json:"low_stock_threshold" form:"low_stock_threshold"`
LowStockAlert uint `gorm:"column:low_stock_alert" json:"low_stock_alert" form:"low_stock_alert"`
Price float32 `gorm:"column:price;type:decimal(20,6)" json:"price" form:"price"`
WholesalePrice float32 `gorm:"column:wholesale_price;type:decimal(20,6)" json:"wholesale_price" form:"wholesale_price"`
Unity string `gorm:"column:unity;type:varchar(255)" json:"unity" form:"unity"`
UnitPriceRatio float32 `gorm:"column:unit_price_ratio;type:decimal(20,6)" json:"unit_price_ratio" form:"unit_price_ratio"`
UnitID uint `gorm:"column:id_unit;primaryKey" json:"unit_id" form:"unit_id"`
AdditionalShippingCost float32 `gorm:"column:additional_shipping_cost;type:decimal(20,2)" json:"additional_shipping_cost" form:"additional_shipping_cost"`
Reference string `gorm:"column:reference;type:varchar(64)" json:"reference" form:"reference"`
SupplierReference string `gorm:"column:supplier_reference;type:varchar(64)" json:"supplier_reference" form:"supplier_reference"`
Location string `gorm:"column:location;type:varchar(64)" json:"location" form:"location"`
Width float32 `gorm:"column:width;type:decimal(20,6)" json:"width" form:"width"`
Height float32 `gorm:"column:height;type:decimal(20,6)" json:"height" form:"height"`
Depth float32 `gorm:"column:depth;type:decimal(20,6)" json:"depth" form:"depth"`
Weight float32 `gorm:"column:weight;type:decimal(20,6)" json:"weight" form:"weight"`
OutOfStock uint `gorm:"column:out_of_stock" json:"out_of_stock" form:"out_of_stock"`
AdditionalDeliveryTimes uint `gorm:"column:additional_delivery_times" json:"additional_delivery_times" form:"additional_delivery_times"`
QuantityDiscount uint `gorm:"column:quantity_discount" json:"quantity_discount" form:"quantity_discount"`
Customizable uint `gorm:"column:customizable" json:"customizable" form:"customizable"`
UploadableFiles uint `gorm:"column:uploadable_files" json:"uploadable_files" form:"uploadable_files"`
TextFields uint `gorm:"column:text_fields" json:"text_fields" form:"text_fields"`
Active uint `gorm:"column:active" json:"active" form:"active"`
RedirectType string `gorm:"column:redirect_type;type:enum('','404','301-product','302-product','301-category','302-category')" json:"redirect_type" form:"redirect_type"`
TypeRedirectedID int `gorm:"column:id_type_redirected" json:"type_redirected_id" form:"type_redirected_id"`
AvailableForOrder uint `gorm:"column:available_for_order" json:"available_for_order" form:"available_for_order"`
AvailableDate string `gorm:"column:available_date;type:date" json:"available_date" form:"available_date"`
ShowCondition uint `gorm:"column:show_condition" json:"show_condition" form:"show_condition"`
Condition string `gorm:"column:condition;type:enum('new','used','refurbished')" json:"condition" form:"condition"`
ShowPrice uint `gorm:"column:show_price" json:"show_price" form:"show_price"`
Indexed uint `gorm:"column:indexed" json:"indexed" form:"indexed"`
Visibility string `gorm:"column:visibility;type:enum('both','catalog','search','none')" json:"visibility" form:"visibility"`
CacheIsPack uint `gorm:"column:cache_is_pack" json:"cache_is_pack" form:"cache_is_pack"`
CacheHasAttachments uint `gorm:"column:cache_has_attachments" json:"cache_has_attachments" form:"cache_has_attachments"`
IsVirtual uint `gorm:"column:is_virtual" json:"is_virtual" form:"is_virtual"`
CacheDefaultAttribute uint `gorm:"column:cache_default_attribute" json:"cache_default_attribute" form:"cache_default_attribute"`
DateAdd string `gorm:"column:date_add;type:datetime" json:"date_add" form:"date_add"`
DateUpd string `gorm:"column:date_upd;type:datetime" json:"date_upd" form:"date_upd"`
AdvancedStockManagement uint `gorm:"column:advanced_stock_management" json:"advanced_stock_management" form:"advanced_stock_management"`
PackStockType uint `gorm:"column:pack_stock_type" json:"pack_stock_type" form:"pack_stock_type"`
State uint `gorm:"column:state" json:"state" form:"state"`
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
}
type ProductInList struct {
ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"`
Name string `gorm:"column:name" json:"name" form:"name"`
@@ -73,6 +12,7 @@ type ProductInList struct {
PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
IsNew uint `gorm:"column:is_new" json:"is_new"`
}
type ProductFilters struct {

View File

@@ -5,7 +5,6 @@ import "time"
type SpecificPrice struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
Scope string `gorm:"type:varchar(20);not null" json:"scope"`
ValidFrom *time.Time `gorm:"null" json:"valid_from"`
ValidTill *time.Time `gorm:"null" json:"valid_till"`
HasExpirationDate bool `gorm:"default:false" json:"has_expiration_date"`

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 (
"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 UICategoryRepo interface {
GetCategoryTranslations(ids []uint, idLang uint) (map[uint]string, error)
RetrieveMenuCategories(idLang uint) ([]model.ScannedCategory, error)
}
type CategoryRepo struct{}
@@ -42,3 +45,33 @@ func (r *CategoryRepo) GetCategoryTranslations(ids []uint, idLang uint) (map[uin
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

@@ -9,7 +9,6 @@ import (
"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"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
"gorm.io/gorm"
)
@@ -17,7 +16,6 @@ type UIProductDescriptionRepo interface {
GetProductDescription(productID uint, productid_lang uint) (*model.ProductDescription, error)
CreateIfDoesNotExist(productID uint, productid_lang uint) error
UpdateFields(productID uint, productid_lang uint, updates map[string]string) error
GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error)
}
type ProductDescriptionRepo struct{}
@@ -118,108 +116,3 @@ func (r *ProductDescriptionRepo) UpdateFields(productID uint, productid_lang uin
return nil
}
// GetMeiliProductsBatchedScanned returns a batch of products with LIMIT/OFFSET pagination
// The scanning is done inside the repo to keep the service layer cleaner
func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct
err := db.Get().
Table("ps_product_shop ps").
Select(`
ps.id_product AS id_product,
pl.name AS name,
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description,
p.ean13,
p.reference,
ps.price,
ps.id_category_default AS id_category,
cl.name AS cat_name,
cl.link_rewrite AS l_rew,
COALESCE(vary.attributes, JSON_OBJECT()) AS attr,
COALESCE(feat.features, JSON_OBJECT()) AS feat,
img.id_image,
cat.category_ids,
(SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = ps.id_product AND pas2.id_shop = ?) AS variations
`, constdata.SHOP_ID).
Joins("JOIN ps_product p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN variations vary ON vary.id_product = ps.id_product").
Joins("LEFT JOIN features feat ON feat.id_product = ps.id_product").
Joins("LEFT JOIN images img ON img.id_product = ps.id_product").
Joins("LEFT JOIN categories cat ON cat.id_product = ps.id_product").
Joins("JOIN products_page pp ON pp.id_product = ps.id_product").
Where("ps.active = ?", 1).
Order("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{
{
Name: "products_page",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductShop{}).
Select("id_product, price").
Where("id_shop = ? AND active = 1", constdata.SHOP_ID).
Order("id_product").
Limit(limit).
Offset(offset),
},
},
{
Name: "variation_attributes",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_product_attribute_shop pas"). // <- explicit alias here
Select(`
pas.id_product,
pag.id_attribute_group AS attribute_name,
JSON_ARRAYAGG(DISTINCT pa.id_attribute) AS attribute_values
`).
Joins("JOIN ps_product_attribute_combination ppac ON ppac.id_product_attribute = pas.id_product_attribute").
Joins("JOIN ps_attribute pa ON pa.id_attribute = ppac.id_attribute").
Joins("JOIN ps_attribute_group pag ON pag.id_attribute_group = pa.id_attribute_group").
Where("pas.id_shop = ?", constdata.SHOP_ID).
Group("pas.id_product, pag.id_attribute_group"),
},
},
{
Name: "variations",
Subquery: exclause.Subquery{
DB: db.Get().
Table("variation_attributes").
Select("id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes").
Group("id_product"),
},
},
{
Name: "features",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_feature_product pfp"). // <- explicit alias
Select("pfp.id_product, JSON_OBJECTAGG(pfp.id_feature, pfp.id_feature_value) AS features").
Group("pfp.id_product"),
},
},
{
Name: "images",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsImageShop{}).
Select("id_product, id_image").
Where("id_shop = ? AND cover = 1", constdata.SHOP_ID),
},
},
{
Name: "categories",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsCategoryProduct{}).
Select("id_product, JSON_ARRAYAGG(id_category) AS category_ids").
Group("id_product"),
},
},
}}).Find(&products).Error
return products, err
}

View File

@@ -105,57 +105,90 @@ func (repo *ProductsRepo) GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_c
}
func (repo *ProductsRepo) Find(langID uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) {
query := db.Get().
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
Select(`
ps.id_product AS product_id,
pl.name AS name,
pl.link_rewrite AS link_rewrite,
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
cl.name AS category_name,
p.reference AS reference,
COALESCE(v.variants_number, 0) AS variants_number,
sa.quantity AS quantity,
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 ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", langID).
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 = ?", langID).
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 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").
Where("ps.active = ?", 1).
Group("ps.id_product").
query := db.DB.
Table("base_products AS bp").
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: "favorites",
Subquery: exclause.Subquery{
DB: db.Get().
DB: db.DB.
Table("b2b_favorites").
Select(`
product_id AS id_product,
product_id AS product_id,
COUNT(*) > 0 AS is_favorite
`).
Where("user_id = ?", userID).
Group("product_id"),
},
},
{
Name: "new_product_days",
Subquery: exclause.Subquery{
DB: db.DB.
Table("ps_configuration").
Select("CAST(value AS SIGNED) AS days").
Where("name = ?", "PS_NB_DAYS_NEW_PRODUCT"),
},
},
{
Name: "variants",
Subquery: exclause.Subquery{
DB: db.DB.
Table("ps_product_attribute_shop").
Select("id_product, COUNT(*) AS variants_number").
Group("id_product"),
},
},
{
Name: "base_products",
Subquery: exclause.Subquery{
DB: db.DB.
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
Select(`
ps.id_product AS product_id,
pl.name AS name,
ps.id_category_default AS category_id,
p.reference AS reference,
sa.quantity AS quantity,
COALESCE(f.is_favorite, 0) AS is_favorite,
CASE
WHEN ps.date_add >= DATE_SUB(
NOW(),
INTERVAL COALESCE(npd.days, 20) DAY
) AND ps.active = 1
THEN 1
ELSE 0
END AS is_new
`).
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 = ?", langID).
Joins("LEFT JOIN favorites f ON f.product_id = 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 new_product_days npd ON 1 = 1").
Where("ps.active = ?", 1).
Group("ps.id_product"),
},
},
},
}).
Order("ps.id_product DESC")
Select(`
bp.product_id AS product_id,
bp.name AS name,
pl.link_rewrite AS link_rewrite,
CONCAT(?, '/', ims.id_image, '-small_default/', pl.link_rewrite, '.webp') AS image_link,
cl.name AS category_name,
bp.reference AS reference,
COALESCE(v.variants_number, 0) AS variants_number,
bp.quantity AS quantity,
bp.is_favorite AS is_favorite,
bp.is_new AS is_new
`, config.Get().Image.ImagePrefix).
Joins("JOIN ps_product_lang pl ON pl.id_product = bp.product_id AND pl.id_lang = ?", langID).
Joins("JOIN ps_image_shop ims ON ims.id_product = bp.product_id AND ims.cover = 1").
Joins("JOIN ps_category_lang cl ON cl.id_category = bp.category_id AND cl.id_lang = ?", langID).
Joins("LEFT JOIN variants v ON v.id_product = bp.product_id").
Order("bp.product_id DESC")
query = query.Scopes(filt.All()...)

View File

@@ -7,7 +7,11 @@ import (
"net/http"
"git.ma-al.com/goc_daniel/b2b/app/config"
"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"
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
)
type SearchProxyResponse struct {
@@ -17,6 +21,7 @@ type SearchProxyResponse struct {
type UISearchRepo interface {
Search(index string, body []byte) (*SearchProxyResponse, error)
GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error)
GetIndexSettings(index string) (*SearchProxyResponse, error)
GetRoutes(langId uint) ([]model.Route, error)
}
@@ -80,3 +85,108 @@ func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyRes
func (r *SearchRepo) GetRoutes(langId uint) ([]model.Route, error) {
return nil, nil
}
// GetMeiliProductsProducts returns a batch of products with LIMIT/OFFSET pagination
// The scanning is done inside the repo to keep the service layer cleaner
func (r *SearchRepo) GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error) {
var products []model.MeiliSearchProduct
err := db.Get().
Table("ps_product_shop ps").
Select(`
ps.id_product AS id_product,
pl.name AS name,
TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description,
p.ean13,
p.reference,
ps.price,
ps.id_category_default AS id_category,
cl.name AS cat_name,
cl.link_rewrite AS l_rew,
COALESCE(vary.attributes, JSON_OBJECT()) AS attr,
COALESCE(feat.features, JSON_OBJECT()) AS feat,
img.id_image,
cat.category_ids,
(SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = ps.id_product AND pas2.id_shop = ?) AS variations
`, constdata.SHOP_ID).
Joins("JOIN ps_product p ON p.id_product = ps.id_product").
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang).
Joins("LEFT JOIN variations vary ON vary.id_product = ps.id_product").
Joins("LEFT JOIN features feat ON feat.id_product = ps.id_product").
Joins("LEFT JOIN images img ON img.id_product = ps.id_product").
Joins("LEFT JOIN categories cat ON cat.id_product = ps.id_product").
Joins("JOIN products_page pp ON pp.id_product = ps.id_product").
Where("ps.active = ?", 1).
Order("ps.id_product").
Clauses(exclause.With{CTEs: []exclause.CTE{
{
Name: "products_page",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsProductShop{}).
Select("id_product, price").
Where("id_shop = ? AND active = 1", constdata.SHOP_ID).
Order("id_product").
Limit(limit).
Offset(offset),
},
},
{
Name: "variation_attributes",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_product_attribute_shop pas"). // <- explicit alias here
Select(`
pas.id_product,
pag.id_attribute_group AS attribute_name,
JSON_ARRAYAGG(DISTINCT pa.id_attribute) AS attribute_values
`).
Joins("JOIN ps_product_attribute_combination ppac ON ppac.id_product_attribute = pas.id_product_attribute").
Joins("JOIN ps_attribute pa ON pa.id_attribute = ppac.id_attribute").
Joins("JOIN ps_attribute_group pag ON pag.id_attribute_group = pa.id_attribute_group").
Where("pas.id_shop = ?", constdata.SHOP_ID).
Group("pas.id_product, pag.id_attribute_group"),
},
},
{
Name: "variations",
Subquery: exclause.Subquery{
DB: db.Get().
Table("variation_attributes").
Select("id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes").
Group("id_product"),
},
},
{
Name: "features",
Subquery: exclause.Subquery{
DB: db.Get().
Table("ps_feature_product pfp"). // <- explicit alias
Select("pfp.id_product, JSON_OBJECTAGG(pfp.id_feature, pfp.id_feature_value) AS features").
Group("pfp.id_product"),
},
},
{
Name: "images",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsImageShop{}).
Select("id_product, id_image").
Where("id_shop = ? AND cover = 1", constdata.SHOP_ID),
},
},
{
Name: "categories",
Subquery: exclause.Subquery{
DB: db.Get().
Model(&dbmodel.PsCategoryProduct{}).
Select("id_product, JSON_ARRAYAGG(id_category) AS category_ids").
Group("id_product"),
},
},
}}).Find(&products).Error
return products, err
}

View File

@@ -6,7 +6,7 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
searchrepo "git.ma-al.com/goc_daniel/b2b/app/repos/searchRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/meilisearch/meilisearch-go"
)
@@ -20,8 +20,8 @@ type MeiliIndexSettings struct {
}
type MeiliService struct {
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo
meiliClient meilisearch.ServiceManager
searchRepo searchrepo.UISearchRepo
meiliClient meilisearch.ServiceManager
}
func New() *MeiliService {
@@ -32,8 +32,8 @@ func New() *MeiliService {
)
return &MeiliService{
meiliClient: client,
productDescriptionRepo: productDescriptionRepo.New(),
meiliClient: client,
searchRepo: searchrepo.New(),
}
}
@@ -50,7 +50,7 @@ func (s *MeiliService) CreateIndex(id_lang uint) error {
for {
// Get batch of products from repo (includes scanning)
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang, offset, batchSize)
products, err := s.searchRepo.GetMeiliProducts(id_lang, offset, batchSize)
if err != nil {
return fmt.Errorf("failed to get products batch at offset %d: %w", offset, err)
}

View File

@@ -3,31 +3,45 @@ package menuService
import (
"slices"
"sort"
"strconv"
"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"
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"
)
type MenuService struct {
categoriesRepo categoriesRepo.UICategoriesRepo
routesRepo routesRepo.UIRoutesRepo
categoryRepo categoryrepo.UICategoryRepo
routesRepo routesRepo.UIRoutesRepo
}
func New() *MenuService {
return &MenuService{
categoriesRepo: categoriesRepo.New(),
routesRepo: routesRepo.New(),
categoryRepo: categoryrepo.New(),
routesRepo: routesRepo.New(),
}
}
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 {
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
root_index := 0
root_found := false
@@ -98,7 +112,7 @@ func (s *MenuService) scannedToNormalCategory(scanned model.ScannedCategory) mod
normal.CategoryID = scanned.CategoryID
normal.Label = scanned.Name
// 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{}
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 (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 {
return []model.CategoryInBreadcrumb{}, err
}
iso_code := all_categories[0].IsoCode
s.appendAdditional(&all_categories, id_lang, iso_code)
breadcrumb := []model.CategoryInBreadcrumb{}
start_index := 0
@@ -211,3 +228,24 @@ func (s *MenuService) GetTopMenu(languageId uint, roleId uint) ([]*model.B2BTopM
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

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

View File

@@ -23,12 +23,6 @@ func (s *SpecificPriceService) Create(ctx context.Context, pr *model.SpecificPri
return nil, err
}
if pr.Scope == "shop" && len(pr.ProductIDs) == 0 && len(pr.CategoryIDs) == 0 && len(pr.ProductAttributeIDs) == 0 && len(pr.CountryIDs) == 0 && len(pr.CustomerIDs) == 0 {
// pr.Scope = "global"
} else {
// pr.Scope = "scoped"
}
if err := s.specificPriceRepo.Create(ctx, pr); err != nil {
return nil, err
}
@@ -51,12 +45,6 @@ func (s *SpecificPriceService) Update(ctx context.Context, id uint64, pr *model.
pr.ID = id
if pr.Scope == "shop" && len(pr.ProductIDs) == 0 && len(pr.CategoryIDs) == 0 && len(pr.ProductAttributeIDs) == 0 && len(pr.CountryIDs) == 0 && len(pr.CustomerIDs) == 0 {
// pr.Scope = "global"
} else {
// pr.Scope = "scoped"
}
if err := s.specificPriceRepo.Update(ctx, pr); err != nil {
return nil, err
}

View File

@@ -3,12 +3,16 @@ 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 DEFAULT_PRODUCT_QUANTITY = 1
const SHOP_DEFAULT_LANGUAGE = 1
const ADMIN_NOTIFICATION_LANGUAGE = 2
// CATEGORY_TREE_ROOT_ID corresponds to id_category in ps_category which has is_root_category=1
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 DEFAULT_NEW_CART_NAME = "new cart"
@@ -25,23 +29,3 @@ const WEBDAV_TRIMMED_ROOT = "localhost:3000/api/v1/webdav/storage"
const NON_ALNUM_REGEX = `[^a-z0-9]+`
const MULTI_DASH_REGEX = `-+`
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

@@ -22,14 +22,6 @@ func GetUserID(c fiber.Ctx) (uint, bool) {
return user_locale.User.ID, true
}
func GetOriginalUserRole(c fiber.Ctx) (model.Role, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.OriginalUser == nil || user_locale.OriginalUser.Role == nil {
return model.Role{}, false
}
return *user_locale.OriginalUser.Role, true
}
func GetCustomer(c fiber.Ctx) (*model.Customer, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.User == nil {

View File

@@ -285,7 +285,6 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrInvalidReductionType),
errors.Is(err, ErrPercentageRequired),
errors.Is(err, ErrPriceRequired),
errors.Is(err, ErrJSONBody),
errors.Is(err, ErrAccessDenied),
errors.Is(err, ErrFolderDoesNotExist),
errors.Is(err, ErrFileDoesNotExist),

View File

@@ -5,7 +5,7 @@ info:
http:
method: GET
url: "{{bas_url}}/restricted/product/list?p=1&elems=30&sort=product_id,asc&reference=~NC100"
url: "{{bas_url}}/restricted/product/list?p=1&elems=30&reference=~NC100"
params:
- name: p
value: "1"
@@ -16,13 +16,22 @@ http:
- name: sort
value: product_id,asc
type: query
disabled: true
- name: category_id_in
value: "243"
value: "23"
type: query
disabled: true
- name: reference
value: ~NC100
type: query
- name: is_new_eq
value: "0"
type: query
disabled: true
- name: is_favorite_eq
value: "false"
type: query
disabled: true
body:
type: json
data: ""

View File

@@ -5,10 +5,10 @@ info:
http:
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:
- name: root_category_id
value: "10"
value: "2"
type: query
- name: category_id
value: "13"

View File

@@ -5,10 +5,10 @@ info:
http:
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:
- name: root_category_id
value: "10"
value: "2"
type: query
auth: inherit

View File

@@ -238,7 +238,6 @@ CREATE TABLE b2b_specific_price (
created_at DATETIME NULL,
updated_at DATETIME NULL,
deleted_at DATETIME NULL,
scope ENUM('shop', 'category', 'product') NOT NULL,
valid_from DATETIME NULL,
valid_till DATETIME NULL,
has_expiration_date BOOLEAN DEFAULT FALSE,
@@ -249,11 +248,9 @@ CREATE TABLE b2b_specific_price (
from_quantity INT UNSIGNED DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE
) ENGINE = InnoDB;
CREATE INDEX idx_b2b_scope ON b2b_specific_price(scope);
CREATE INDEX idx_b2b_active_dates ON b2b_specific_price(is_active, valid_from, valid_till);
CREATE INDEX idx_b2b_lookup
ON b2b_specific_price (
scope,
is_active,
from_quantity
);
@@ -307,11 +304,11 @@ ON b2b_specific_price_category (id_category);
CREATE INDEX idx_b2b_product_attribute_rel
ON b2b_specific_price_product_attribute (id_product_attribute);
CREATE INDEX idx_bsp_customer
ON b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer);
CREATE INDEX idx_bsp_customer_rel
ON b2b_specific_price_customer (b2b_id_customer);
CREATE INDEX idx_bsp_country
ON b2b_specific_price_country (b2b_specific_price_id, b2b_id_country);
CREATE INDEX idx_bsp_country_rel
ON b2b_specific_price_country (b2b_id_country);
DELIMITER //
@@ -405,16 +402,26 @@ DELIMITER ;
-- +goose Down
DROP TABLE IF EXISTS b2b_countries;
DROP TABLE IF EXISTS b2b_language;
DROP TABLE IF EXISTS b2b_components;
DROP TABLE IF EXISTS b2b_scopes;
DROP TABLE IF EXISTS b2b_translations;
DROP TABLE IF EXISTS b2b_customers;
DROP TABLE IF EXISTS b2b_refresh_tokens;
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_addresses;
DROP TABLE IF EXISTS b2b_top_menu_roles;
DROP TABLE IF EXISTS b2b_favorites;
DROP TABLE IF EXISTS b2b_carts_products;
DROP TABLE IF EXISTS b2b_customer_carts;
DROP TABLE IF EXISTS b2b_specific_price_country;
DROP TABLE IF EXISTS b2b_specific_price_customer;
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

@@ -34,13 +34,28 @@ INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('1', 'user.read.any');
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('2', 'user.write.any');
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('3', 'user.delete.any');
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('4', 'currency.write');
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('5', 'specific_price.manage');
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('6', 'webdav.create_token');
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('7', 'product_translation.save');
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('8', 'product_translation.translate');
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('9', 'search.create_index');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '1');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '2');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '3');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '4');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '5');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '6');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '7');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '8');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '9');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '1');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '2');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '3');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '4');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '5');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '6');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '7');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '8');
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '9');
-- +goose Down

View File

@@ -1,9 +0,0 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd