From 438a13c04c840e4cb649d3c6adca7a489b2b2be5 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 10:34:44 +0200 Subject: [PATCH 01/16] orders tables --- .../20260302163122_create_tables.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index ba4469a..6040571 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -151,6 +151,33 @@ CREATE TABLE IF NOT EXISTS b2b_carts_products ( CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (cart_id); +-- customer_orders +CREATE TABLE IF NOT EXISTS b2b_customer_orders ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + country_id BIGINT UNSIGNED NOT NULL, + address_json TEXT NULL, + status VARCHAR(50) NULL, + CONSTRAINT fk_customer_orders_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE NO ACTION ON UPDATE CASCADE, + CONSTRAINT fk_customer_orders_countries FOREIGN KEY (country_id) REFERENCES b2b_countries(id) ON DELETE NO ACTION ON UPDATE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; +CREATE INDEX idx_customer_orders_user_id ON b2b_customer_orders (user_id); +CREATE INDEX idx_customer_orders_country_id ON b2b_customer_orders (country_id); + + +-- orders_products +CREATE TABLE IF NOT EXISTS b2b_orders_products ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + order_id BIGINT UNSIGNED NOT NULL, + product_id INT UNSIGNED NOT NULL, + product_attribute_id INT NULL, + amount INT UNSIGNED NOT NULL, + CONSTRAINT fk_orders_products_customer_orders FOREIGN KEY (order_id) REFERENCES b2b_customer_orders (id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_orders_products_product FOREIGN KEY (product_id) REFERENCES ps_product (id_product) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; +CREATE INDEX IF NOT EXISTS idx_orders_products_order_id ON b2b_orders_products (order_id); + + -- favorites CREATE TABLE IF NOT EXISTS b2b_favorites ( user_id BIGINT UNSIGNED NOT NULL, From dfdf8b4db99e4b7eff54cce79b485d8ce4bd2106 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 11:01:39 +0200 Subject: [PATCH 02/16] orders handler init --- app/delivery/web/api/restricted/orders.go | 148 ++++++++++++++++++++++ app/delivery/web/init.go | 4 + app/model/order.go | 0 3 files changed, 152 insertions(+) create mode 100644 app/delivery/web/api/restricted/orders.go create mode 100644 app/model/order.go diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go new file mode 100644 index 0000000..4cb054a --- /dev/null +++ b/app/delivery/web/api/restricted/orders.go @@ -0,0 +1,148 @@ +package restricted + +import ( + "strconv" + + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "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/query/query_params" + "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" +) + +type OrdersHandler struct{} + +func NewOrdersHandler() *OrdersHandler { + return &OrdersHandler{} +} + +func OrdersHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewOrdersHandler() + + r.Get("/list", handler.ListOrders) + r.Get("/place-new-order", handler.PlaceNewOrder) + r.Get("/change-order-address", handler.ChangeOrderAddress) + r.Get("/change-order-status", handler.ChangeOrderStatus) + + return r +} + +// when a user (not admin) wants to list orders, we automatically append filter to only view his orders. +// we base permissions and user based on target user only. +func (h *OrdersHandler) ListOrders(c fiber.Ctx) error { + user, ok := localeExtractor.GetCustomer(c) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + + paging, filters, err := query_params.ParseFilters[model.Order](c, columnMappingListOrders) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + list, err := h.ordersService.Find(user, paging, filters) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&list.Items, int(list.Count), i18n.T_(c, response.Message_OK))) +} + +var columnMappingListOrders map[string]string = map[string]string{ + "order_id": "?", +} + +func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { + 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))) + } + + cart_id_attribute := c.Query("cart_id") + cart_id, err := strconv.Atoi(cart_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + address_info := c.Query("address_info") + country_id_attribute := c.Query("country_id") + country_id, err := strconv.Atoi(country_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + err = h.ordersService.PlaceNewOrder(userID, cart_id, country_id, address_info) + 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))) +} + +// we base permissions and user based on target user only. +func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error { + user, ok := localeExtractor.GetCustomer(c) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + + order_id_attribute := c.Query("order_id") + order_id, err := strconv.Atoi(order_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + address_info := c.Query("address_info") + country_id_attribute := c.Query("country_id") + country_id, err := strconv.Atoi(country_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + err = h.ordersService.ChangeOrderAddress(user, order_id, country_id, address_info) + 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))) +} + +// we base permissions and user based on target user only. +func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error { + user, ok := localeExtractor.GetCustomer(c) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + + order_id_attribute := c.Query("order_id") + order_id, err := strconv.Atoi(order_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + status := c.Query("status") + + err = h.ordersService.ChangeOrderStatus(user, order_id, status) + 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))) +} diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index 2162d66..eb7a4c3 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -132,6 +132,10 @@ func (s *Server) Setup() error { carts := s.restricted.Group("/carts") restricted.CartsHandlerRoutes(carts) + // orders (restricted) + orders := s.restricted.Group("/orders") + restricted.OrdersHandlerRoutes(orders) + // addresses (restricted) addresses := s.restricted.Group("/addresses") restricted.AddressesHandlerRoutes(addresses) diff --git a/app/model/order.go b/app/model/order.go new file mode 100644 index 0000000..e69de29 From 4f4b32b131a2203af0aaac38d3892d6b536913d6 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 11:06:43 +0200 Subject: [PATCH 03/16] order struct --- app/delivery/web/api/restricted/orders.go | 7 ++++-- app/model/order.go | 26 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go index 4cb054a..8b21eba 100644 --- a/app/delivery/web/api/restricted/orders.go +++ b/app/delivery/web/api/restricted/orders.go @@ -39,7 +39,7 @@ func (h *OrdersHandler) ListOrders(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } - paging, filters, err := query_params.ParseFilters[model.Order](c, columnMappingListOrders) + paging, filters, err := query_params.ParseFilters[model.CustomerOrder](c, columnMappingListOrders) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -55,7 +55,10 @@ func (h *OrdersHandler) ListOrders(c fiber.Ctx) error { } var columnMappingListOrders map[string]string = map[string]string{ - "order_id": "?", + "order_id": "co.id", + "user_id": "co.user_id", + "country_id": "co.country_id", + "status": "co.status", } func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { diff --git a/app/model/order.go b/app/model/order.go index e69de29..e01a9e0 100644 --- a/app/model/order.go +++ b/app/model/order.go @@ -0,0 +1,26 @@ +package model + +type CustomerOrder struct { + ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + UserID uint `gorm:"column:user_id;not null;index" json:"-"` + CountryID uint `gorm:"column:country_id;not null" json:"country_id"` + AddressJSON *string `gorm:"column:address_json" json:"address_json,omitempty"` + Status *string `gorm:"column:status;size:50" json:"status,omitempty"` + Products []OrderProduct `gorm:"foreignKey:OrderID;references:ID" json:"products,omitempty"` +} + +func (CustomerOrder) TableName() string { + return "b2b_customer_orders" +} + +type OrderProduct struct { + ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"-"` + OrderID uint `gorm:"column:order_id;not null;index" json:"-"` + ProductID uint `gorm:"column:product_id;not null" json:"product_id"` + ProductAttributeID *uint `gorm:"column:product_attribute_id" json:"product_attribute_id,omitempty"` + Amount uint `gorm:"column:amount;not null" json:"amount"` +} + +func (OrderProduct) TableName() string { + return "b2b_orders_products" +} From a6aa06faa0de67e45057f0607b6b16014d07a137 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 11:17:58 +0200 Subject: [PATCH 04/16] basic setup --- app/delivery/web/api/restricted/orders.go | 16 ++++++---- app/repos/ordersRepo/ordersRepo.go | 36 +++++++++++++++++++++++ app/service/orderService/orderService.go | 34 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 app/repos/ordersRepo/ordersRepo.go create mode 100644 app/service/orderService/orderService.go diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go index 8b21eba..3c5a180 100644 --- a/app/delivery/web/api/restricted/orders.go +++ b/app/delivery/web/api/restricted/orders.go @@ -4,6 +4,7 @@ import ( "strconv" "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/service/orderService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" @@ -13,10 +14,15 @@ import ( "github.com/gofiber/fiber/v3" ) -type OrdersHandler struct{} +type OrdersHandler struct { + ordersService *orderService.OrderService +} func NewOrdersHandler() *OrdersHandler { - return &OrdersHandler{} + ordersService := orderService.New() + return &OrdersHandler{ + ordersService: ordersService, + } } func OrdersHandlerRoutes(r fiber.Router) fiber.Router { @@ -83,7 +89,7 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - err = h.ordersService.PlaceNewOrder(userID, cart_id, country_id, address_info) + err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), uint(country_id), address_info) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -115,7 +121,7 @@ func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - err = h.ordersService.ChangeOrderAddress(user, order_id, country_id, address_info) + err = h.ordersService.ChangeOrderAddress(user, uint(order_id), uint(country_id), address_info) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) @@ -141,7 +147,7 @@ func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error { status := c.Query("status") - err = h.ordersService.ChangeOrderStatus(user, order_id, status) + err = h.ordersService.ChangeOrderStatus(user, uint(order_id), status) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/repos/ordersRepo/ordersRepo.go b/app/repos/ordersRepo/ordersRepo.go new file mode 100644 index 0000000..e4832e9 --- /dev/null +++ b/app/repos/ordersRepo/ordersRepo.go @@ -0,0 +1,36 @@ +package ordersRepo + +import ( + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" +) + +type UIOrdersRepo interface { + Find(user_id uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) + PlaceNewOrder(user_id uint, cart_id uint, country_id uint, address_info string) error + ChangeOrderAddress(user_id uint, order_id uint, country_id uint, address_info string) error + ChangeOrderStatus(user_id uint, order_id uint, status string) error +} + +type OrdersRepo struct{} + +func New() UIOrdersRepo { + return &OrdersRepo{} +} + +func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) { + return find.Found[model.CustomerOrder]{}, nil +} + +func (repo *OrdersRepo) PlaceNewOrder(user_id uint, cart_id uint, country_id uint, address_info string) error { + return nil +} + +func (repo *OrdersRepo) ChangeOrderAddress(user_id uint, order_id uint, country_id uint, address_info string) error { + return nil +} + +func (repo *OrdersRepo) ChangeOrderStatus(user_id uint, order_id uint, status string) error { + return nil +} diff --git a/app/service/orderService/orderService.go b/app/service/orderService/orderService.go new file mode 100644 index 0000000..6b1fb09 --- /dev/null +++ b/app/service/orderService/orderService.go @@ -0,0 +1,34 @@ +package orderService + +import ( + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/ordersRepo" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" +) + +type OrderService struct { + ordersRepo ordersRepo.UIOrdersRepo +} + +func New() *OrderService { + return &OrderService{ + ordersRepo: ordersRepo.New(), + } +} + +func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) { + return s.ordersRepo.Find(user.ID, p, filt) +} + +func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, country_id uint, address_info string) error { + return nil +} + +func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error { + return nil +} + +func (s *OrderService) ChangeOrderStatus(user *model.Customer, order_id uint, status string) error { + return nil +} From 8595969c6ef0ae8e2653bb201bf5934aa9473dc5 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 12:17:52 +0200 Subject: [PATCH 05/16] Find is done --- app/delivery/middleware/perms/permissions.go | 1 + app/delivery/web/api/restricted/orders.go | 11 ++--- app/repos/ordersRepo/ordersRepo.go | 34 +++++++++++++- .../addressesService/addressesService.go | 8 ++-- app/service/orderService/orderService.go | 44 +++++++++++++++++-- app/utils/responseErrors/responseErrors.go | 4 ++ 6 files changed, 89 insertions(+), 13 deletions(-) diff --git a/app/delivery/middleware/perms/permissions.go b/app/delivery/middleware/perms/permissions.go index 7528921..7fa5458 100644 --- a/app/delivery/middleware/perms/permissions.go +++ b/app/delivery/middleware/perms/permissions.go @@ -7,4 +7,5 @@ const ( UserWriteAny Permission = "user.write.any" UserDeleteAny Permission = "user.delete.any" CurrencyWrite Permission = "currency.write" + ViewAllOrders Permission = "orders.view" ) diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go index 3c5a180..28e8266 100644 --- a/app/delivery/web/api/restricted/orders.go +++ b/app/delivery/web/api/restricted/orders.go @@ -61,10 +61,10 @@ func (h *OrdersHandler) ListOrders(c fiber.Ctx) error { } var columnMappingListOrders map[string]string = map[string]string{ - "order_id": "co.id", - "user_id": "co.user_id", - "country_id": "co.country_id", - "status": "co.status", + "order_id": "b2b_customer_orders.id", + "user_id": "b2b_customer_orders.user_id", + "country_id": "b2b_customer_orders.country_id", + "status": "b2b_customer_orders.status", } func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { @@ -81,7 +81,6 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - address_info := c.Query("address_info") country_id_attribute := c.Query("country_id") country_id, err := strconv.Atoi(country_id_attribute) if err != nil { @@ -89,6 +88,8 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } + address_info := c.Query("address_info") + err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), uint(country_id), address_info) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). diff --git a/app/repos/ordersRepo/ordersRepo.go b/app/repos/ordersRepo/ordersRepo.go index e4832e9..d299976 100644 --- a/app/repos/ordersRepo/ordersRepo.go +++ b/app/repos/ordersRepo/ordersRepo.go @@ -1,6 +1,7 @@ package ordersRepo 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/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" @@ -20,10 +21,41 @@ func New() UIOrdersRepo { } func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) { - return find.Found[model.CustomerOrder]{}, nil + var list []model.CustomerOrder + var total int64 + + query := db.Get(). + Model(&model.CustomerOrder{}). + Preload("Products"). + Order("b2b_customer_orders.id DESC") + + // Apply all filters + if filt != nil { + filt.ApplyAll(query) + } + + // run counter first as query is without limit and offset + err := query.Count(&total).Error + if err != nil { + return find.Found[model.CustomerOrder]{}, err + } + + err = query. + Limit(p.Limit()). + Offset(p.Offset()). + Find(&list).Error + if err != nil { + return find.Found[model.CustomerOrder]{}, err + } + + return find.Found[model.CustomerOrder]{ + Items: list, + Count: uint(total), + }, nil } func (repo *OrdersRepo) PlaceNewOrder(user_id uint, cart_id uint, country_id uint, address_info string) error { + return nil } diff --git a/app/service/addressesService/addressesService.go b/app/service/addressesService/addressesService.go index b077486..48eadc3 100644 --- a/app/service/addressesService/addressesService.go +++ b/app/service/addressesService/addressesService.go @@ -49,7 +49,7 @@ func (s *AddressesService) AddNewAddress(user_id uint, address_info string, coun return responseErrors.ErrMaxAmtOfAddressesReached } - _, err = s.validateAddressJson(address_info, country_id) + _, err = s.ValidateAddressJson(address_info, country_id) if err != nil { return err } @@ -66,7 +66,7 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_ return responseErrors.ErrUserHasNoSuchAddress } - _, err = s.validateAddressJson(address_info, country_id) + _, err = s.ValidateAddressJson(address_info, country_id) if err != nil { return err } @@ -88,7 +88,7 @@ func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.Address next_address.CustomerID = (*parsed_addresses)[i].CustomerID next_address.CountryID = (*parsed_addresses)[i].CountryID - next_address.AddressInfo, err = s.validateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID) + next_address.AddressInfo, err = s.ValidateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID) // log such errors if err != nil { fmt.Printf("err: %v\n", err) @@ -112,7 +112,7 @@ func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error { } // validateAddressJson makes sure that the info string represents a valid json of address in given country -func (s *AddressesService) validateAddressJson(info string, country_id uint) (model.AddressField, error) { +func (s *AddressesService) ValidateAddressJson(info string, country_id uint) (model.AddressField, error) { dec := json.NewDecoder(strings.NewReader(info)) dec.DisallowUnknownFields() diff --git a/app/service/orderService/orderService.go b/app/service/orderService/orderService.go index 6b1fb09..ccd46e0 100644 --- a/app/service/orderService/orderService.go +++ b/app/service/orderService/orderService.go @@ -1,28 +1,66 @@ package orderService import ( + "strconv" + + "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/repos/cartsRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/ordersRepo" + "git.ma-al.com/goc_daniel/b2b/app/service/addressesService" "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/responseErrors" ) type OrderService struct { - ordersRepo ordersRepo.UIOrdersRepo + ordersRepo ordersRepo.UIOrdersRepo + cartsRepo cartsRepo.UICartsRepo + addressesService *addressesService.AddressesService } func New() *OrderService { return &OrderService{ - ordersRepo: ordersRepo.New(), + ordersRepo: ordersRepo.New(), + cartsRepo: cartsRepo.New(), + addressesService: addressesService.New(), } } func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) { + if !user.HasPermission(perms.ViewAllOrders) { + // append filter to view only this user's orders + idStr := strconv.FormatUint(uint64(user.ID), 10) + filt.Append(filters.Where("b2b_customer_orders.user_id = " + idStr)) + } + return s.ordersRepo.Find(user.ID, p, filt) } func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, country_id uint, address_info string) error { - return nil + _, err := s.addressesService.ValidateAddressJson(address_info, country_id) + if err != nil { + return err + } + + amt, err := s.cartsRepo.UserHasCart(user_id, cart_id) + if err != nil { + return err + } + if amt <= 0 { + return responseErrors.ErrUserHasNoSuchCart + } + + cart, err := s.cartsRepo.RetrieveCart(user_id, cart_id) + if err != nil { + return err + } + if len(cart.Products) == 0 { + return responseErrors.ErrEmptyCart + } + + // all checks passed + return s.ordersRepo.PlaceNewOrder(user_id, cart_id, country_id, address_info) } func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error { diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index 6b3c548..752e532 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -65,6 +65,7 @@ var ( ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") + ErrEmptyCart = errors.New("the cart is empty") // Typed errors for storage ErrAccessDenied = errors.New("access denied!") @@ -195,6 +196,8 @@ func GetErrorCode(c fiber.Ctx, err error) string { return i18n.T_(c, "error.err_user_has_no_such_cart") case errors.Is(err, ErrProductOrItsVariationDoesNotExist): return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist") + case errors.Is(err, ErrEmptyCart): + return i18n.T_(c, "error.err_cart_is_empty") case errors.Is(err, ErrAccessDenied): return i18n.T_(c, "error.err_access_denied") @@ -268,6 +271,7 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrProductOrItsVariationDoesNotExist), + errors.Is(err, ErrEmptyCart), errors.Is(err, ErrAccessDenied), errors.Is(err, ErrFolderDoesNotExist), errors.Is(err, ErrFileDoesNotExist), From 134bc4ea53931b679a8adf55293a77cb35615ea3 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 13:23:51 +0200 Subject: [PATCH 06/16] code ready, time for testing... --- app/delivery/middleware/perms/permissions.go | 11 ++-- app/delivery/web/api/restricted/orders.go | 8 ++- app/model/order.go | 12 ++-- app/repos/cartsRepo/cartsRepo.go | 14 ++--- app/repos/ordersRepo/ordersRepo.go | 60 ++++++++++++++++--- app/service/orderService/orderService.go | 43 +++++++++++-- app/utils/const_data/consts.go | 3 + app/utils/responseErrors/responseErrors.go | 9 ++- .../20260302163122_create_tables.sql | 15 ++--- 9 files changed, 131 insertions(+), 44 deletions(-) diff --git a/app/delivery/middleware/perms/permissions.go b/app/delivery/middleware/perms/permissions.go index 7fa5458..40c10d4 100644 --- a/app/delivery/middleware/perms/permissions.go +++ b/app/delivery/middleware/perms/permissions.go @@ -3,9 +3,10 @@ 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" - ViewAllOrders Permission = "orders.view" + UserReadAny Permission = "user.read.any" + UserWriteAny Permission = "user.write.any" + UserDeleteAny Permission = "user.delete.any" + CurrencyWrite Permission = "currency.write" + ViewAllOrders Permission = "orders.view" + ModifyAllOrders Permission = "orders.modify" ) diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go index 28e8266..0d0ef51 100644 --- a/app/delivery/web/api/restricted/orders.go +++ b/app/delivery/web/api/restricted/orders.go @@ -61,8 +61,9 @@ func (h *OrdersHandler) ListOrders(c fiber.Ctx) error { } var columnMappingListOrders map[string]string = map[string]string{ - "order_id": "b2b_customer_orders.id", + "order_id": "b2b_customer_orders.order_id", "user_id": "b2b_customer_orders.user_id", + "name": "b2b_customer_orders.name", "country_id": "b2b_customer_orders.country_id", "status": "b2b_customer_orders.status", } @@ -81,6 +82,7 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } + address_info := c.Query("address_info") country_id_attribute := c.Query("country_id") country_id, err := strconv.Atoi(country_id_attribute) if err != nil { @@ -88,9 +90,9 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - address_info := c.Query("address_info") + name := c.Query("name") - err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), uint(country_id), address_info) + err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), name, uint(country_id), address_info) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) diff --git a/app/model/order.go b/app/model/order.go index e01a9e0..8fcb9b6 100644 --- a/app/model/order.go +++ b/app/model/order.go @@ -1,12 +1,13 @@ package model type CustomerOrder struct { - ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - UserID uint `gorm:"column:user_id;not null;index" json:"-"` + OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_id"` + UserID uint `gorm:"column:user_id;not null;index" json:"user_id"` + Name string `gorm:"column:name;not null" json:"name"` CountryID uint `gorm:"column:country_id;not null" json:"country_id"` - AddressJSON *string `gorm:"column:address_json" json:"address_json,omitempty"` - Status *string `gorm:"column:status;size:50" json:"status,omitempty"` - Products []OrderProduct `gorm:"foreignKey:OrderID;references:ID" json:"products,omitempty"` + AddressJSON string `gorm:"column:address_json;not null" json:"address_json"` + Status string `gorm:"column:status;size:50;not null" json:"status"` + Products []OrderProduct `gorm:"foreignKey:OrderID;references:ID" json:"products"` } func (CustomerOrder) TableName() string { @@ -14,7 +15,6 @@ func (CustomerOrder) TableName() string { } type OrderProduct struct { - ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"-"` OrderID uint `gorm:"column:order_id;not null;index" json:"-"` ProductID uint `gorm:"column:product_id;not null" json:"product_id"` ProductAttributeID *uint `gorm:"column:product_attribute_id" json:"product_attribute_id,omitempty"` diff --git a/app/repos/cartsRepo/cartsRepo.go b/app/repos/cartsRepo/cartsRepo.go index b15700c..dab8bee 100644 --- a/app/repos/cartsRepo/cartsRepo.go +++ b/app/repos/cartsRepo/cartsRepo.go @@ -9,11 +9,11 @@ import ( type UICartsRepo interface { CartsAmount(user_id uint) (uint, error) CreateNewCart(user_id uint) (model.CustomerCart, error) - UserHasCart(user_id uint, cart_id uint) (uint, error) + UserHasCart(user_id uint, cart_id uint) (bool, error) UpdateCartName(user_id uint, cart_id uint, new_name string) error RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) - CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) + CheckProductExists(product_id uint, product_attribute_id *uint) (bool, error) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error } @@ -49,7 +49,7 @@ func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) { return cart, err } -func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) { +func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (bool, error) { var amt uint err := db.DB. @@ -59,7 +59,7 @@ func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) { Scan(&amt). Error - return amt, err + return amt >= 1, err } func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error { @@ -96,7 +96,7 @@ func (repo *CartsRepo) RetrieveCart(user_id uint, cart_id uint) (*model.Customer return &cart, err } -func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) { +func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (bool, error) { var amt uint if product_attribute_id == nil { @@ -106,7 +106,7 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id Where("id_product = ?", product_id). Scan(&amt). Error - return amt, err + return amt >= 1, err } else { err := db.DB. @@ -116,7 +116,7 @@ func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id). Scan(&amt). Error - return amt, err + return amt >= 1, err } } diff --git a/app/repos/ordersRepo/ordersRepo.go b/app/repos/ordersRepo/ordersRepo.go index d299976..a530293 100644 --- a/app/repos/ordersRepo/ordersRepo.go +++ b/app/repos/ordersRepo/ordersRepo.go @@ -3,15 +3,17 @@ package ordersRepo import ( "git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/model" + 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/find" ) type UIOrdersRepo interface { + UserHasOrder(user_id uint, order_id uint) (bool, error) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) - PlaceNewOrder(user_id uint, cart_id uint, country_id uint, address_info string) error - ChangeOrderAddress(user_id uint, order_id uint, country_id uint, address_info string) error - ChangeOrderStatus(user_id uint, order_id uint, status string) error + PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error + ChangeOrderAddress(order_id uint, country_id uint, address_info string) error + ChangeOrderStatus(order_id uint, status string) error } type OrdersRepo struct{} @@ -20,6 +22,19 @@ func New() UIOrdersRepo { return &OrdersRepo{} } +func (repo *OrdersRepo) UserHasOrder(user_id uint, order_id uint) (bool, error) { + var amt uint + + err := db.DB. + Table("b2b_customer_orders"). + Select("COUNT(*) AS amt"). + Where("user_id = ? AND order_id = ?", user_id, order_id). + Scan(&amt). + Error + + return amt >= 1, err +} + func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) { var list []model.CustomerOrder var total int64 @@ -54,15 +69,42 @@ func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersL }, nil } -func (repo *OrdersRepo) PlaceNewOrder(user_id uint, cart_id uint, country_id uint, address_info string) error { +func (repo *OrdersRepo) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error { + order := model.CustomerOrder{ + UserID: cart.UserID, + Name: name, + CountryID: country_id, + AddressJSON: address_info, + Status: constdata.NEW_ORDER_STATUS, + Products: make([]model.OrderProduct, 0, len(cart.Products)), + } - return nil + for _, product := range cart.Products { + order.Products = append(order.Products, model.OrderProduct{ + ProductID: product.ProductID, + ProductAttributeID: product.ProductAttributeID, + Amount: product.Amount, + }) + } + + return db.DB.Create(&order).Error } -func (repo *OrdersRepo) ChangeOrderAddress(user_id uint, order_id uint, country_id uint, address_info string) error { - return nil +func (repo *OrdersRepo) ChangeOrderAddress(order_id uint, country_id uint, address_info string) error { + return db.DB. + Table("b2b_customer_orders"). + Where("order_id = ?", order_id). + Updates(map[string]interface{}{ + "country_id": country_id, + "address_info": address_info, + }). + Error } -func (repo *OrdersRepo) ChangeOrderStatus(user_id uint, order_id uint, status string) error { - return nil +func (repo *OrdersRepo) ChangeOrderStatus(order_id uint, status string) error { + return db.DB. + Table("b2b_customer_orders"). + Where("order_id = ?", order_id). + Update("status", status). + Error } diff --git a/app/service/orderService/orderService.go b/app/service/orderService/orderService.go index ccd46e0..397f226 100644 --- a/app/service/orderService/orderService.go +++ b/app/service/orderService/orderService.go @@ -37,17 +37,17 @@ func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.F return s.ordersRepo.Find(user.ID, p, filt) } -func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, country_id uint, address_info string) error { +func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, country_id uint, address_info string) error { _, err := s.addressesService.ValidateAddressJson(address_info, country_id) if err != nil { return err } - amt, err := s.cartsRepo.UserHasCart(user_id, cart_id) + exists, err := s.cartsRepo.UserHasCart(user_id, cart_id) if err != nil { return err } - if amt <= 0 { + if !exists { return responseErrors.ErrUserHasNoSuchCart } @@ -59,14 +59,45 @@ func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, country_id uint return responseErrors.ErrEmptyCart } + if name == "" && cart.Name != nil { + name = *cart.Name + } + // all checks passed - return s.ordersRepo.PlaceNewOrder(user_id, cart_id, country_id, address_info) + return s.ordersRepo.PlaceNewOrder(cart, name, country_id, address_info) } func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error { - return nil + _, err := s.addressesService.ValidateAddressJson(address_info, country_id) + if err != nil { + return err + } + + if !user.HasPermission(perms.ModifyAllOrders) { + exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id) + if err != nil { + return err + } + + if !exists { + return responseErrors.ErrUserHasNoSuchOrder + } + } + + return s.ordersRepo.ChangeOrderAddress(order_id, country_id, address_info) } func (s *OrderService) ChangeOrderStatus(user *model.Customer, order_id uint, status string) error { - return nil + if !user.HasPermission(perms.ModifyAllOrders) { + exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id) + if err != nil { + return err + } + + if !exists { + return responseErrors.ErrUserHasNoSuchOrder + } + } + + return s.ordersRepo.ChangeOrderStatus(order_id, status) } diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index aa62f27..547698c 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -16,6 +16,9 @@ const MAX_AMOUNT_OF_ADDRESSES_PER_USER = 10 const USER_LOCALE = "user" +// ORDERS +const NEW_ORDER_STATUS = "PENDING" + // WEBDAV const NBYTES_IN_WEBDAV_TOKEN = 32 const WEBDAV_HREF_ROOT = "http://localhost:3000/api/v1/webdav/storage" diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index 752e532..4d14a50 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -65,7 +65,10 @@ var ( ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") - ErrEmptyCart = errors.New("the cart is empty") + + // Typed errors for orders handler + ErrEmptyCart = errors.New("the cart is empty") + ErrUserHasNoSuchOrder = errors.New("user does not have order with given id") // Typed errors for storage ErrAccessDenied = errors.New("access denied!") @@ -196,8 +199,11 @@ func GetErrorCode(c fiber.Ctx, err error) string { return i18n.T_(c, "error.err_user_has_no_such_cart") case errors.Is(err, ErrProductOrItsVariationDoesNotExist): return i18n.T_(c, "error.err_product_or_its_variation_does_not_exist") + case errors.Is(err, ErrEmptyCart): return i18n.T_(c, "error.err_cart_is_empty") + case errors.Is(err, ErrUserHasNoSuchOrder): + return i18n.T_(c, "error.err_user_has_no_such_order") case errors.Is(err, ErrAccessDenied): return i18n.T_(c, "error.err_access_denied") @@ -272,6 +278,7 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrProductOrItsVariationDoesNotExist), errors.Is(err, ErrEmptyCart), + errors.Is(err, ErrUserHasNoSuchOrder), errors.Is(err, ErrAccessDenied), errors.Is(err, ErrFolderDoesNotExist), errors.Is(err, ErrFileDoesNotExist), diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index 6040571..8c1cf30 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -130,7 +130,7 @@ FOREIGN KEY (role_id) REFERENCES b2b_roles(id); -- customer_carts CREATE TABLE IF NOT EXISTS b2b_customer_carts ( - cart_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + cart_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, name VARCHAR(255) NULL, CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE @@ -140,8 +140,8 @@ CREATE INDEX IF NOT EXISTS idx_customer_carts_user_id ON b2b_customer_carts (use -- carts_products CREATE TABLE IF NOT EXISTS b2b_carts_products ( - id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, - cart_id INT UNSIGNED NOT NULL, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + cart_id BIGINT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, product_attribute_id INT NULL, amount INT UNSIGNED NOT NULL, @@ -153,11 +153,12 @@ CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (car -- customer_orders CREATE TABLE IF NOT EXISTS b2b_customer_orders ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + order_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, + name TEXT NOT NULL, country_id BIGINT UNSIGNED NOT NULL, - address_json TEXT NULL, - status VARCHAR(50) NULL, + address_json TEXT NOT NULL, + status VARCHAR(50) NOT NULL, CONSTRAINT fk_customer_orders_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE NO ACTION ON UPDATE CASCADE, CONSTRAINT fk_customer_orders_countries FOREIGN KEY (country_id) REFERENCES b2b_countries(id) ON DELETE NO ACTION ON UPDATE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; @@ -167,7 +168,7 @@ CREATE INDEX idx_customer_orders_country_id ON b2b_customer_orders (country_id); -- orders_products CREATE TABLE IF NOT EXISTS b2b_orders_products ( - id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, product_attribute_id INT NULL, From a03a2b461fa32371c6d6728dcaccafda933513ab Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 13:25:00 +0200 Subject: [PATCH 07/16] small fix --- app/service/cartsService/cartsService.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/service/cartsService/cartsService.go b/app/service/cartsService/cartsService.go index c82e7b2..235c412 100644 --- a/app/service/cartsService/cartsService.go +++ b/app/service/cartsService/cartsService.go @@ -35,11 +35,11 @@ func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) { } func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error { - amt, err := s.repo.UserHasCart(user_id, cart_id) + exists, err := s.repo.UserHasCart(user_id, cart_id) if err != nil { return err } - if amt != 1 { + if !exists { return responseErrors.ErrUserHasNoSuchCart } @@ -51,11 +51,11 @@ func (s *CartsService) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, er } func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) { - amt, err := s.repo.UserHasCart(user_id, cart_id) + exists, err := s.repo.UserHasCart(user_id, cart_id) if err != nil { return nil, err } - if amt != 1 { + if !exists { return nil, responseErrors.ErrUserHasNoSuchCart } @@ -63,19 +63,19 @@ func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.Customer } func (s *CartsService) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error { - amt, err := s.repo.UserHasCart(user_id, cart_id) + exists, err := s.repo.UserHasCart(user_id, cart_id) if err != nil { return err } - if amt != 1 { + if !exists { return responseErrors.ErrUserHasNoSuchCart } - amt, err = s.repo.CheckProductExists(product_id, product_attribute_id) + exists, err = s.repo.CheckProductExists(product_id, product_attribute_id) if err != nil { return err } - if amt != 1 { + if !exists { return responseErrors.ErrProductOrItsVariationDoesNotExist } From 33e9d016e909ae3f9266fe784ba8476736f621fb Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 14:53:40 +0200 Subject: [PATCH 08/16] basic orders are ready --- app/delivery/web/api/restricted/addresses.go | 4 +- app/delivery/web/api/restricted/orders.go | 16 ++++- app/model/address.go | 19 ++---- app/model/order.go | 15 ++--- app/repos/addressesRepo/addressesRepo.go | 14 ++--- app/repos/ordersRepo/ordersRepo.go | 28 ++++----- .../addressesService/addressesService.go | 23 +++----- app/service/orderService/orderService.go | 20 ++++++- bruno/b2b_daniel/carts/retrieve-cart.yml | 4 +- .../orders/change-order-address.yml | 33 +++++++++++ .../b2b_daniel/orders/change-order-status.yml | 22 +++++++ bruno/b2b_daniel/orders/folder.yml | 7 +++ bruno/b2b_daniel/orders/list.yml | 31 ++++++++++ bruno/b2b_daniel/orders/place-new-order.yml | 37 ++++++++++++ .../20260302163122_create_tables.sql | 58 +++++++++---------- 15 files changed, 237 insertions(+), 94 deletions(-) create mode 100644 bruno/b2b_daniel/orders/change-order-address.yml create mode 100644 bruno/b2b_daniel/orders/change-order-status.yml create mode 100644 bruno/b2b_daniel/orders/folder.yml create mode 100644 bruno/b2b_daniel/orders/list.yml create mode 100644 bruno/b2b_daniel/orders/place-new-order.yml diff --git a/app/delivery/web/api/restricted/addresses.go b/app/delivery/web/api/restricted/addresses.go index 903f011..f33b7cb 100644 --- a/app/delivery/web/api/restricted/addresses.go +++ b/app/delivery/web/api/restricted/addresses.go @@ -124,13 +124,13 @@ func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) } - addresses_info, err := h.addressesService.RetrieveAddressesInfo(userID) + addresses, err := h.addressesService.RetrieveAddresses(userID) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) } - return c.JSON(response.Make(&addresses_info, 0, i18n.T_(c, response.Message_OK))) + return c.JSON(response.Make(addresses, 0, i18n.T_(c, response.Message_OK))) } func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error { diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go index 0d0ef51..34fb2c5 100644 --- a/app/delivery/web/api/restricted/orders.go +++ b/app/delivery/web/api/restricted/orders.go @@ -29,7 +29,7 @@ func OrdersHandlerRoutes(r fiber.Router) fiber.Router { handler := NewOrdersHandler() r.Get("/list", handler.ListOrders) - r.Get("/place-new-order", handler.PlaceNewOrder) + r.Post("/place-new-order", handler.PlaceNewOrder) r.Get("/change-order-address", handler.ChangeOrderAddress) r.Get("/change-order-status", handler.ChangeOrderStatus) @@ -82,7 +82,6 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - address_info := c.Query("address_info") country_id_attribute := c.Query("country_id") country_id, err := strconv.Atoi(country_id_attribute) if err != nil { @@ -90,6 +89,12 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } + address_info := string(c.Body()) + if address_info == "" { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + name := c.Query("name") err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), name, uint(country_id), address_info) @@ -116,7 +121,6 @@ func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } - address_info := c.Query("address_info") country_id_attribute := c.Query("country_id") country_id, err := strconv.Atoi(country_id_attribute) if err != nil { @@ -124,6 +128,12 @@ func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error { JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) } + address_info := string(c.Body()) + if address_info == "" { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + err = h.ordersService.ChangeOrderAddress(user, uint(order_id), uint(country_id), address_info) if err != nil { return c.Status(responseErrors.GetErrorStatus(err)). diff --git a/app/model/address.go b/app/model/address.go index a84056a..96efaa8 100644 --- a/app/model/address.go +++ b/app/model/address.go @@ -1,25 +1,18 @@ package model type Address struct { - ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` - AddressInfo string `gorm:"column:address_info;not null" json:"address_info"` - CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` + ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` + AddressString string `gorm:"column:address_string;not null" json:"address_string"` + AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"` + CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` } func (Address) TableName() string { return "b2b_addresses" } -type AddressUnparsed struct { - ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"` - CustomerID uint `gorm:"column:b2b_customer_id;not null;index" json:"customer_id"` - AddressInfo AddressField `gorm:"column:address_info;not null" json:"address_info"` - CountryID uint `gorm:"column:b2b_country_id;not null" json:"country_id"` -} - -type AddressField interface { -} +type AddressUnparsed interface{} // Address template in Poland type AddressPL struct { diff --git a/app/model/order.go b/app/model/order.go index 8fcb9b6..a6556b2 100644 --- a/app/model/order.go +++ b/app/model/order.go @@ -1,13 +1,14 @@ package model type CustomerOrder struct { - OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_id"` - UserID uint `gorm:"column:user_id;not null;index" json:"user_id"` - Name string `gorm:"column:name;not null" json:"name"` - CountryID uint `gorm:"column:country_id;not null" json:"country_id"` - AddressJSON string `gorm:"column:address_json;not null" json:"address_json"` - Status string `gorm:"column:status;size:50;not null" json:"status"` - Products []OrderProduct `gorm:"foreignKey:OrderID;references:ID" json:"products"` + OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_id"` + UserID uint `gorm:"column:user_id;not null;index" json:"user_id"` + Name string `gorm:"column:name;not null" json:"name"` + CountryID uint `gorm:"column:country_id;not null" json:"country_id"` + AddressString string `gorm:"column:address_string;not null" json:"address_string"` + AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"` + Status string `gorm:"column:status;size:50;not null" json:"status"` + Products []OrderProduct `gorm:"foreignKey:OrderID;references:OrderID" json:"products"` } func (CustomerOrder) TableName() string { diff --git a/app/repos/addressesRepo/addressesRepo.go b/app/repos/addressesRepo/addressesRepo.go index 5f674a0..ce6437d 100644 --- a/app/repos/addressesRepo/addressesRepo.go +++ b/app/repos/addressesRepo/addressesRepo.go @@ -48,9 +48,9 @@ func (repo *AddressesRepo) UserAddressesAmt(user_id uint) (uint, error) { func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, country_id uint) error { address := model.Address{ - CustomerID: user_id, - AddressInfo: address_info, - CountryID: country_id, + CustomerID: user_id, + AddressString: address_info, + CountryID: country_id, } return db.DB. @@ -60,10 +60,10 @@ func (repo *AddressesRepo) AddNewAddress(user_id uint, address_info string, coun func (repo *AddressesRepo) UpdateAddress(user_id uint, address_id uint, address_info string, country_id uint) error { address := model.Address{ - ID: address_id, - CustomerID: user_id, - AddressInfo: address_info, - CountryID: country_id, + ID: address_id, + CustomerID: user_id, + AddressString: address_info, + CountryID: country_id, } return db.DB. diff --git a/app/repos/ordersRepo/ordersRepo.go b/app/repos/ordersRepo/ordersRepo.go index a530293..c3b8158 100644 --- a/app/repos/ordersRepo/ordersRepo.go +++ b/app/repos/ordersRepo/ordersRepo.go @@ -10,7 +10,7 @@ import ( type UIOrdersRepo interface { UserHasOrder(user_id uint, order_id uint) (bool, error) - Find(user_id uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) + Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error ChangeOrderAddress(order_id uint, country_id uint, address_info string) error ChangeOrderStatus(order_id uint, status string) error @@ -35,14 +35,14 @@ func (repo *OrdersRepo) UserHasOrder(user_id uint, order_id uint) (bool, error) return amt >= 1, err } -func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) { +func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) { var list []model.CustomerOrder var total int64 query := db.Get(). Model(&model.CustomerOrder{}). Preload("Products"). - Order("b2b_customer_orders.id DESC") + Order("b2b_customer_orders.order_id DESC") // Apply all filters if filt != nil { @@ -52,7 +52,7 @@ func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersL // run counter first as query is without limit and offset err := query.Count(&total).Error if err != nil { - return find.Found[model.CustomerOrder]{}, err + return &find.Found[model.CustomerOrder]{}, err } err = query. @@ -60,10 +60,10 @@ func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersL Offset(p.Offset()). Find(&list).Error if err != nil { - return find.Found[model.CustomerOrder]{}, err + return &find.Found[model.CustomerOrder]{}, err } - return find.Found[model.CustomerOrder]{ + return &find.Found[model.CustomerOrder]{ Items: list, Count: uint(total), }, nil @@ -71,12 +71,12 @@ func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersL func (repo *OrdersRepo) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error { order := model.CustomerOrder{ - UserID: cart.UserID, - Name: name, - CountryID: country_id, - AddressJSON: address_info, - Status: constdata.NEW_ORDER_STATUS, - Products: make([]model.OrderProduct, 0, len(cart.Products)), + UserID: cart.UserID, + Name: name, + CountryID: country_id, + AddressString: address_info, + Status: constdata.NEW_ORDER_STATUS, + Products: make([]model.OrderProduct, 0, len(cart.Products)), } for _, product := range cart.Products { @@ -95,8 +95,8 @@ func (repo *OrdersRepo) ChangeOrderAddress(order_id uint, country_id uint, addre Table("b2b_customer_orders"). Where("order_id = ?", order_id). Updates(map[string]interface{}{ - "country_id": country_id, - "address_info": address_info, + "country_id": country_id, + "address_string": address_info, }). Error } diff --git a/app/service/addressesService/addressesService.go b/app/service/addressesService/addressesService.go index 48eadc3..0b7a820 100644 --- a/app/service/addressesService/addressesService.go +++ b/app/service/addressesService/addressesService.go @@ -21,7 +21,7 @@ func New() *AddressesService { } } -func (s *AddressesService) GetTemplate(country_id uint) (model.AddressField, error) { +func (s *AddressesService) GetTemplate(country_id uint) (model.AddressUnparsed, error) { switch country_id { case 1: // Poland @@ -74,30 +74,23 @@ func (s *AddressesService) ModifyAddress(user_id uint, address_id uint, address_ return s.repo.UpdateAddress(user_id, address_id, address_info, country_id) } -func (s *AddressesService) RetrieveAddressesInfo(user_id uint) (*[]model.AddressUnparsed, error) { - parsed_addresses, err := s.repo.RetrieveAddresses(user_id) +func (s *AddressesService) RetrieveAddresses(user_id uint) (*[]model.Address, error) { + addresses, err := s.repo.RetrieveAddresses(user_id) if err != nil { return nil, err } - var unparsed_addresses []model.AddressUnparsed - - for i := 0; i < len(*parsed_addresses); i++ { - var next_address model.AddressUnparsed - next_address.ID = (*parsed_addresses)[i].ID - next_address.CustomerID = (*parsed_addresses)[i].CustomerID - next_address.CountryID = (*parsed_addresses)[i].CountryID - - next_address.AddressInfo, err = s.ValidateAddressJson((*parsed_addresses)[i].AddressInfo, next_address.CountryID) + for i := 0; i < len(*addresses); i++ { + address_unparsed, err := s.ValidateAddressJson((*addresses)[i].AddressString, (*addresses)[i].CountryID) // log such errors if err != nil { fmt.Printf("err: %v\n", err) } - unparsed_addresses = append(unparsed_addresses, next_address) + (*addresses)[i].AddressUnparsed = &address_unparsed } - return &unparsed_addresses, nil + return addresses, nil } func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error { @@ -112,7 +105,7 @@ func (s *AddressesService) DeleteAddress(user_id uint, address_id uint) error { } // validateAddressJson makes sure that the info string represents a valid json of address in given country -func (s *AddressesService) ValidateAddressJson(info string, country_id uint) (model.AddressField, error) { +func (s *AddressesService) ValidateAddressJson(info string, country_id uint) (model.AddressUnparsed, error) { dec := json.NewDecoder(strings.NewReader(info)) dec.DisallowUnknownFields() diff --git a/app/service/orderService/orderService.go b/app/service/orderService/orderService.go index 397f226..9d831e5 100644 --- a/app/service/orderService/orderService.go +++ b/app/service/orderService/orderService.go @@ -1,6 +1,7 @@ package orderService import ( + "fmt" "strconv" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" @@ -27,14 +28,29 @@ func New() *OrderService { } } -func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (find.Found[model.CustomerOrder], error) { +func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) { if !user.HasPermission(perms.ViewAllOrders) { // append filter to view only this user's orders idStr := strconv.FormatUint(uint64(user.ID), 10) filt.Append(filters.Where("b2b_customer_orders.user_id = " + idStr)) } - return s.ordersRepo.Find(user.ID, p, filt) + list, err := s.ordersRepo.Find(user.ID, p, filt) + if err != nil { + return nil, err + } + + for i := 0; i < len(list.Items); i++ { + address_unparsed, err := s.addressesService.ValidateAddressJson(list.Items[i].AddressString, list.Items[i].CountryID) + // log such errors + if err != nil { + fmt.Printf("err: %v\n", err) + } + + list.Items[i].AddressUnparsed = &address_unparsed + } + + return list, nil } func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, country_id uint, address_info string) error { diff --git a/bruno/b2b_daniel/carts/retrieve-cart.yml b/bruno/b2b_daniel/carts/retrieve-cart.yml index 69c5e2e..809b524 100644 --- a/bruno/b2b_daniel/carts/retrieve-cart.yml +++ b/bruno/b2b_daniel/carts/retrieve-cart.yml @@ -5,10 +5,10 @@ info: http: method: GET - url: http://localhost:3000/api/v1/restricted/carts/retrieve-cart?cart_id=3 + url: http://localhost:3000/api/v1/restricted/carts/retrieve-cart?cart_id=1 params: - name: cart_id - value: "3" + value: "1" type: query auth: inherit diff --git a/bruno/b2b_daniel/orders/change-order-address.yml b/bruno/b2b_daniel/orders/change-order-address.yml new file mode 100644 index 0000000..e6d8856 --- /dev/null +++ b/bruno/b2b_daniel/orders/change-order-address.yml @@ -0,0 +1,33 @@ +info: + name: change-order-address + type: http + seq: 3 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/orders/change-order-address?order_id=1&country_id=1 + params: + - name: order_id + value: "1" + type: query + - name: country_id + value: "1" + type: query + body: + type: json + data: |- + { + "postal_code": "31-154", + "city": "Kraków", + "voivodeship": "śląskie", + "street": "Długa", + "building_no": "5", + "recipient": "Adam Adamowicz" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/orders/change-order-status.yml b/bruno/b2b_daniel/orders/change-order-status.yml new file mode 100644 index 0000000..78c0e74 --- /dev/null +++ b/bruno/b2b_daniel/orders/change-order-status.yml @@ -0,0 +1,22 @@ +info: + name: change-order-status + type: http + seq: 4 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/orders/change-order-status?order_id=1&status=PAID + params: + - name: order_id + value: "1" + type: query + - name: status + value: PAID + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/orders/folder.yml b/bruno/b2b_daniel/orders/folder.yml new file mode 100644 index 0000000..f542e59 --- /dev/null +++ b/bruno/b2b_daniel/orders/folder.yml @@ -0,0 +1,7 @@ +info: + name: orders + type: folder + seq: 11 + +request: + auth: inherit diff --git a/bruno/b2b_daniel/orders/list.yml b/bruno/b2b_daniel/orders/list.yml new file mode 100644 index 0000000..ce7e339 --- /dev/null +++ b/bruno/b2b_daniel/orders/list.yml @@ -0,0 +1,31 @@ +info: + name: list + type: http + seq: 2 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/orders/list?p=1&elems=30&sort=product_id,asc&user_id=2&name=~sdj + params: + - name: p + value: "1" + type: query + - name: elems + value: "30" + type: query + - name: sort + value: product_id,asc + type: query + - name: user_id + value: "2" + type: query + - name: name + value: ~sdj + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b_daniel/orders/place-new-order.yml b/bruno/b2b_daniel/orders/place-new-order.yml new file mode 100644 index 0000000..80ef063 --- /dev/null +++ b/bruno/b2b_daniel/orders/place-new-order.yml @@ -0,0 +1,37 @@ +info: + name: place-new-order + type: http + seq: 1 + +http: + method: POST + url: http://localhost:3000/api/v1/restricted/orders/place-new-order?cart_id=1&name=sdjalksd&country_id=1 + params: + - name: cart_id + value: "1" + type: query + - name: name + value: sdjalksd + type: query + - name: country_id + value: "1" + type: query + body: + type: json + data: |- + { + "postal_code": "31-154", + "city": "Kraków", + "voivodeship": "małopolskie", + "street": "Długa", + "building_no": "5", + "apartment_no": "7", + "recipient": "Jan Kowalski" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index 8c1cf30..a2dcdbe 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -151,34 +151,6 @@ CREATE TABLE IF NOT EXISTS b2b_carts_products ( CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (cart_id); --- customer_orders -CREATE TABLE IF NOT EXISTS b2b_customer_orders ( - order_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, - user_id BIGINT UNSIGNED NOT NULL, - name TEXT NOT NULL, - country_id BIGINT UNSIGNED NOT NULL, - address_json TEXT NOT NULL, - status VARCHAR(50) NOT NULL, - CONSTRAINT fk_customer_orders_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE NO ACTION ON UPDATE CASCADE, - CONSTRAINT fk_customer_orders_countries FOREIGN KEY (country_id) REFERENCES b2b_countries(id) ON DELETE NO ACTION ON UPDATE CASCADE -) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -CREATE INDEX idx_customer_orders_user_id ON b2b_customer_orders (user_id); -CREATE INDEX idx_customer_orders_country_id ON b2b_customer_orders (country_id); - - --- orders_products -CREATE TABLE IF NOT EXISTS b2b_orders_products ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, - order_id BIGINT UNSIGNED NOT NULL, - product_id INT UNSIGNED NOT NULL, - product_attribute_id INT NULL, - amount INT UNSIGNED NOT NULL, - CONSTRAINT fk_orders_products_customer_orders FOREIGN KEY (order_id) REFERENCES b2b_customer_orders (id) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT fk_orders_products_product FOREIGN KEY (product_id) REFERENCES ps_product (id_product) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -CREATE INDEX IF NOT EXISTS idx_orders_products_order_id ON b2b_orders_products (order_id); - - -- favorites CREATE TABLE IF NOT EXISTS b2b_favorites ( user_id BIGINT UNSIGNED NOT NULL, @@ -252,7 +224,7 @@ ON `b2b_countries` ( CREATE TABLE IF NOT EXISTS b2b_addresses ( id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, b2b_customer_id BIGINT UNSIGNED NOT NULL, - address_info TEXT NOT NULL, + address_string TEXT NOT NULL, b2b_country_id BIGINT UNSIGNED NOT NULL, PRIMARY KEY (id), CONSTRAINT fk_b2b_addresses_b2b_customers FOREIGN KEY (b2b_customer_id) REFERENCES b2b_customers (id) ON DELETE CASCADE ON UPDATE CASCADE, @@ -260,6 +232,34 @@ CREATE TABLE IF NOT EXISTS b2b_addresses ( ) ENGINE = InnoDB; +-- customer_orders +CREATE TABLE IF NOT EXISTS b2b_customer_orders ( + order_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + name TEXT NOT NULL, + country_id BIGINT UNSIGNED NOT NULL, + address_string TEXT NOT NULL, + status VARCHAR(50) NOT NULL, + CONSTRAINT fk_customer_orders_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE NO ACTION ON UPDATE CASCADE, + CONSTRAINT fk_customer_orders_countries FOREIGN KEY (country_id) REFERENCES b2b_countries(id) ON DELETE NO ACTION ON UPDATE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; +CREATE INDEX idx_customer_orders_user_id ON b2b_customer_orders (user_id); +CREATE INDEX idx_customer_orders_country_id ON b2b_customer_orders (country_id); + + +-- orders_products +CREATE TABLE IF NOT EXISTS b2b_orders_products ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + order_id BIGINT UNSIGNED NOT NULL, + product_id INT UNSIGNED NOT NULL, + product_attribute_id INT NULL, + amount INT UNSIGNED NOT NULL, + CONSTRAINT fk_orders_products_customer_orders FOREIGN KEY (order_id) REFERENCES b2b_customer_orders (order_id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_orders_products_product FOREIGN KEY (product_id) REFERENCES ps_product (id_product) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; +CREATE INDEX IF NOT EXISTS idx_orders_products_order_id ON b2b_orders_products (order_id); + + CREATE TABLE b2b_specific_price ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, From 80d26bba1256c2396948ab1a847f864e90e12cbf Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 14:57:24 +0200 Subject: [PATCH 09/16] GET -> POST --- app/delivery/web/api/restricted/orders.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go index 34fb2c5..751d712 100644 --- a/app/delivery/web/api/restricted/orders.go +++ b/app/delivery/web/api/restricted/orders.go @@ -30,7 +30,7 @@ func OrdersHandlerRoutes(r fiber.Router) fiber.Router { r.Get("/list", handler.ListOrders) r.Post("/place-new-order", handler.PlaceNewOrder) - r.Get("/change-order-address", handler.ChangeOrderAddress) + r.Post("/change-order-address", handler.ChangeOrderAddress) r.Get("/change-order-status", handler.ChangeOrderStatus) return r From d4d55e275758b12f7f0f9cf6bd9e39260b9ec29d Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 10 Apr 2026 15:17:29 +0200 Subject: [PATCH 10/16] send email when creating new order --- app/service/emailService/email.go | 19 ++++++++++++++ app/service/orderService/orderService.go | 17 +++++++++++- .../emailNewOrderPlacedNotification.templ | 26 +++++++++++++++++++ app/view/emails.go | 4 +++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 app/templ/emails/emailNewOrderPlacedNotification.templ diff --git a/app/service/emailService/email.go b/app/service/emailService/email.go index 29cc9bb..2c92117 100644 --- a/app/service/emailService/email.go +++ b/app/service/emailService/email.go @@ -117,6 +117,18 @@ func (s *EmailService) SendNewUserAdminNotification(userEmail, userName, baseURL return s.SendEmail(s.config.AdminEmail, subject, body) } +// SendNewOrderPlacedNotification sends an email to admin when new order is placed +func (s *EmailService) SendNewOrderPlacedNotification(userID uint) error { + if s.config.AdminEmail == "" { + return nil // No admin email configured + } + + subject := "New Order Created" + body := s.newOrderPlacedTemplate(userID) + + return s.SendEmail(s.config.AdminEmail, subject, body) +} + // verificationEmailTemplate returns the HTML template for email verification func (s *EmailService) verificationEmailTemplate(name, verificationURL string, langID uint) string { buf := bytes.Buffer{} @@ -137,3 +149,10 @@ func (s *EmailService) newUserAdminNotificationTemplate(userEmail, userName, bas emails.EmailAdminNotificationWrapper(view.EmailLayout[view.EmailAdminNotificationData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailAdminNotificationData{UserEmail: userEmail, UserName: userName, BaseURL: baseURL}}).Render(context.Background(), &buf) return buf.String() } + +// newUserAdminNotificationTemplate returns the HTML template for admin notification +func (s *EmailService) newOrderPlacedTemplate(userID uint) string { + buf := bytes.Buffer{} + emails.EmailNewOrderPlacedWrapper(view.EmailLayout[view.EmailNewOrderPlacedData]{LangID: constdata.ADMIN_NOTIFICATION_LANGUAGE, Data: view.EmailNewOrderPlacedData{UserID: userID}}).Render(context.Background(), &buf) + return buf.String() +} diff --git a/app/service/orderService/orderService.go b/app/service/orderService/orderService.go index 9d831e5..eb0e2f2 100644 --- a/app/service/orderService/orderService.go +++ b/app/service/orderService/orderService.go @@ -9,6 +9,7 @@ import ( "git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/ordersRepo" "git.ma-al.com/goc_daniel/b2b/app/service/addressesService" + "git.ma-al.com/goc_daniel/b2b/app/service/emailService" "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/responseErrors" @@ -18,6 +19,7 @@ type OrderService struct { ordersRepo ordersRepo.UIOrdersRepo cartsRepo cartsRepo.UICartsRepo addressesService *addressesService.AddressesService + emailService *emailService.EmailService } func New() *OrderService { @@ -25,6 +27,7 @@ func New() *OrderService { ordersRepo: ordersRepo.New(), cartsRepo: cartsRepo.New(), addressesService: addressesService.New(), + emailService: emailService.NewEmailService(), } } @@ -80,7 +83,19 @@ func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, co } // all checks passed - return s.ordersRepo.PlaceNewOrder(cart, name, country_id, address_info) + err = s.ordersRepo.PlaceNewOrder(cart, name, country_id, address_info) + if err != nil { + return err + } + + // send email to admin + err = s.emailService.SendNewOrderPlacedNotification(user_id) + if err != nil { + // Log error but don't fail placing order??? + _ = err + } + + return nil } func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error { diff --git a/app/templ/emails/emailNewOrderPlacedNotification.templ b/app/templ/emails/emailNewOrderPlacedNotification.templ new file mode 100644 index 0000000..73b9ed4 --- /dev/null +++ b/app/templ/emails/emailNewOrderPlacedNotification.templ @@ -0,0 +1,26 @@ +package emails + +import ( + "git.ma-al.com/goc_daniel/b2b/app/templ/layout" + "git.ma-al.com/goc_daniel/b2b/app/view" + "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" +) + +templ EmailNewOrderPlacedWrapper(data view.EmailLayout[view.EmailNewOrderPlacedData]) { + @layout.Base( i18n.T___(data.LangID, "email.email_new_order_placed_notification_title")) { +
+ +
+ } +} \ No newline at end of file diff --git a/app/view/emails.go b/app/view/emails.go index c488dc9..892d989 100644 --- a/app/view/emails.go +++ b/app/view/emails.go @@ -18,3 +18,7 @@ type EmailAdminNotificationData struct { type EmailPasswordResetData struct { ResetURL string } + +type EmailNewOrderPlacedData struct { + UserID uint +} From 1f6d5ecb726682898073089b54c7829f0be6a792 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Mon, 13 Apr 2026 09:21:33 +0200 Subject: [PATCH 11/16] go routine and removing cart --- app/delivery/web/api/restricted/orders.go | 1 + app/repos/cartsRepo/cartsRepo.go | 9 +++++++++ app/service/cartsService/cartsService.go | 12 ++++++++++++ app/service/orderService/orderService.go | 16 +++++++++++++--- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/delivery/web/api/restricted/orders.go b/app/delivery/web/api/restricted/orders.go index 751d712..186599c 100644 --- a/app/delivery/web/api/restricted/orders.go +++ b/app/delivery/web/api/restricted/orders.go @@ -144,6 +144,7 @@ func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error { } // we base permissions and user based on target user only. +// TODO: well, permissions and all that. func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error { user, ok := localeExtractor.GetCustomer(c) if !ok { diff --git a/app/repos/cartsRepo/cartsRepo.go b/app/repos/cartsRepo/cartsRepo.go index dab8bee..4141dcb 100644 --- a/app/repos/cartsRepo/cartsRepo.go +++ b/app/repos/cartsRepo/cartsRepo.go @@ -9,6 +9,7 @@ import ( type UICartsRepo interface { CartsAmount(user_id uint) (uint, error) CreateNewCart(user_id uint) (model.CustomerCart, error) + RemoveCart(user_id uint, cart_id uint) error UserHasCart(user_id uint, cart_id uint) (bool, error) UpdateCartName(user_id uint, cart_id uint, new_name string) error RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) @@ -49,6 +50,14 @@ func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) { return cart, err } +func (repo *CartsRepo) RemoveCart(user_id uint, cart_id uint) error { + return db.DB. + Table("b2b_customer_carts"). + Where("cart_id = ? AND user_id = ?", cart_id, user_id). + Delete(nil). + Error +} + func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (bool, error) { var amt uint diff --git a/app/service/cartsService/cartsService.go b/app/service/cartsService/cartsService.go index 235c412..2ed4006 100644 --- a/app/service/cartsService/cartsService.go +++ b/app/service/cartsService/cartsService.go @@ -34,6 +34,18 @@ func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) { return cart, nil } +func (s *CartsService) RemoveCart(user_id uint, cart_id uint) error { + exists, err := s.repo.UserHasCart(user_id, cart_id) + if err != nil { + return err + } + if !exists { + return responseErrors.ErrUserHasNoSuchCart + } + + return s.repo.RemoveCart(user_id, cart_id) +} + func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error { exists, err := s.repo.UserHasCart(user_id, cart_id) if err != nil { diff --git a/app/service/orderService/orderService.go b/app/service/orderService/orderService.go index eb0e2f2..da21785 100644 --- a/app/service/orderService/orderService.go +++ b/app/service/orderService/orderService.go @@ -87,14 +87,24 @@ func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, co if err != nil { return err } + // from this point onward we do not cancel this order. - // send email to admin - err = s.emailService.SendNewOrderPlacedNotification(user_id) + // if no error is returned, remove the cart. This should be smooth + err = s.cartsRepo.RemoveCart(user_id, cart_id) if err != nil { - // Log error but don't fail placing order??? + // Log error but don't fail placing order _ = err } + // send email to admin + go func(user_id uint) { + err := s.emailService.SendNewOrderPlacedNotification(user_id) + if err != nil { + // Log error but don't fail placing order + _ = err + } + }(user_id) + return nil } From 88255776f38f1b68ac429c9179ecc1ea65637699 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Mon, 13 Apr 2026 14:29:36 +0200 Subject: [PATCH 12/16] fixes --- app/model/category.go | 4 +- app/repos/categoriesRepo/categoriesRepo.go | 48 ----------------- app/repos/categoryRepo/categoryRepo.go | 33 ++++++++++++ app/service/menuService/menuService.go | 54 ++++++++++++++++--- .../sanitizeURLSlug.go | 21 ++++---- app/utils/const_data/consts.go | 23 ++------ bruno/b2b_daniel/menu/get-breadcrumb.yml | 4 +- bruno/b2b_daniel/menu/get-category-tree.yml | 4 +- .../20260302163122_create_tables.sql | 34 +++++++----- 9 files changed, 121 insertions(+), 104 deletions(-) delete mode 100644 app/repos/categoriesRepo/categoriesRepo.go diff --git a/app/model/category.go b/app/model/category.go index 50e6ce9..8ba7ee1 100644 --- a/app/model/category.go +++ b/app/model/category.go @@ -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 { diff --git a/app/repos/categoriesRepo/categoriesRepo.go b/app/repos/categoriesRepo/categoriesRepo.go deleted file mode 100644 index 955292a..0000000 --- a/app/repos/categoriesRepo/categoriesRepo.go +++ /dev/null @@ -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 -} diff --git a/app/repos/categoryRepo/categoryRepo.go b/app/repos/categoryRepo/categoryRepo.go index dd31f39..086cfcf 100644 --- a/app/repos/categoryRepo/categoryRepo.go +++ b/app/repos/categoryRepo/categoryRepo.go @@ -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 +} diff --git a/app/service/menuService/menuService.go b/app/service/menuService/menuService.go index de2498d..e63b6b5 100644 --- a/app/service/menuService/menuService.go +++ b/app/service/menuService/menuService.go @@ -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) +} diff --git a/app/service/productTranslationService/sanitizeURLSlug.go b/app/service/productTranslationService/sanitizeURLSlug.go index ea69d7c..2d0ddbf 100644 --- a/app/service/productTranslationService/sanitizeURLSlug.go +++ b/app/service/productTranslationService/sanitizeURLSlug.go @@ -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 { diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index aa62f27..2d760bc 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -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 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 +28,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", -} diff --git a/bruno/b2b_daniel/menu/get-breadcrumb.yml b/bruno/b2b_daniel/menu/get-breadcrumb.yml index a805790..5870773 100644 --- a/bruno/b2b_daniel/menu/get-breadcrumb.yml +++ b/bruno/b2b_daniel/menu/get-breadcrumb.yml @@ -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" diff --git a/bruno/b2b_daniel/menu/get-category-tree.yml b/bruno/b2b_daniel/menu/get-category-tree.yml index 6e9d875..a1b6714 100644 --- a/bruno/b2b_daniel/menu/get-category-tree.yml +++ b/bruno/b2b_daniel/menu/get-category-tree.yml @@ -5,10 +5,10 @@ info: http: method: GET - url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=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 diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index ba4469a..0ec6408 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -405,16 +405,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; From ce4cadaa1628bc8e0ce5cdcb159f99a47445a9de Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Mon, 13 Apr 2026 15:29:21 +0200 Subject: [PATCH 13/16] most importantly: new category and filter on is_new --- app/delivery/middleware/auth.go | 21 ---- app/delivery/middleware/perms/permissions.go | 14 ++- app/delivery/web/api/restricted/product.go | 1 + .../web/api/restricted/productTranslation.go | 18 +-- app/delivery/web/api/restricted/search.go | 10 +- app/delivery/web/api/restricted/storage.go | 10 +- app/delivery/web/init.go | 10 -- app/model/product.go | 1 + .../productDescriptionRepo.go | 107 ----------------- app/repos/productsRepo/productsRepo.go | 14 ++- app/repos/searchRepo/searchRepo.go | 110 ++++++++++++++++++ app/service/meiliService/meiliService.go | 12 +- app/utils/localeExtractor/localeExtractor.go | 8 -- .../20260302163123_create_tables_data.sql | 12 ++ 14 files changed, 159 insertions(+), 189 deletions(-) diff --git a/app/delivery/middleware/auth.go b/app/delivery/middleware/auth.go index 756e79f..54910ea 100644 --- a/app/delivery/middleware/auth.go +++ b/app/delivery/middleware/auth.go @@ -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() diff --git a/app/delivery/middleware/perms/permissions.go b/app/delivery/middleware/perms/permissions.go index 69ab5df..fb922d4 100644 --- a/app/delivery/middleware/perms/permissions.go +++ b/app/delivery/middleware/perms/permissions.go @@ -3,9 +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" - SpecificPriceManage Permission = "specific_price.manage" + UserReadAny Permission = "user.read.any" + UserWriteAny Permission = "user.write.any" + UserDeleteAny Permission = "user.delete.any" + CurrencyWrite Permission = "currency.write" + SpecificPriceManage Permission = "specific_price.manage" + CreateWebdavToken Permission = "webdav.create_token" + ProductTranslationSave Permission = "product_translation.save" + ProductTranslationTranslate Permission = "product_translation.translate" + SearchCreateIndex Permission = "search.create_index" ) diff --git a/app/delivery/web/api/restricted/product.go b/app/delivery/web/api/restricted/product.go index ea0e07f..bc7778e 100644 --- a/app/delivery/web/api/restricted/product.go +++ b/app/delivery/web/api/restricted/product.go @@ -111,6 +111,7 @@ var columnMappingListProducts map[string]string = map[string]string{ "category_id": "cp.id_category", "quantity": "sa.quantity", "is_favorite": "ps.is_favorite", + "is_new": "is_new", } func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error { diff --git a/app/delivery/web/api/restricted/productTranslation.go b/app/delivery/web/api/restricted/productTranslation.go index 3dc16bd..c995085 100644 --- a/app/delivery/web/api/restricted/productTranslation.go +++ b/app/delivery/web/api/restricted/productTranslation.go @@ -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/delivery/middleware" "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 +35,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("product_translation.save"), handler.SaveProductDescription) + r.Get("/translate-product-description", middleware.Require("product_translation.translate"), handler.TranslateProductDescription) return r } @@ -80,12 +80,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 +117,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 { diff --git a/app/delivery/web/api/restricted/search.go b/app/delivery/web/api/restricted/search.go index 843c956..dc79683 100644 --- a/app/delivery/web/api/restricted/search.go +++ b/app/delivery/web/api/restricted/search.go @@ -4,7 +4,7 @@ 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/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 +30,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("search.create_index"), handler.CreateIndex) r.Post("/search", handler.Search) r.Post("/settings", handler.GetSettings) @@ -44,12 +44,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) diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index 910aae1..1bd631d 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -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/delivery/middleware" "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 +34,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("webdav.create_token"), handler.CreateNewWebdavToken) return r } @@ -84,12 +84,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)). diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index 29fcd71..c67d530 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -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()) diff --git a/app/model/product.go b/app/model/product.go index 8c062a7..6595f79 100644 --- a/app/model/product.go +++ b/app/model/product.go @@ -12,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 { diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go index 5083a42..b6eda1e 100644 --- a/app/repos/productDescriptionRepo/productDescriptionRepo.go +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -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 -} diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index 5f0e1a9..eae3f91 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -116,7 +116,19 @@ func (repo *ProductsRepo) Find(langID uint, userID uint, p find.Paging, filt *fi p.reference AS reference, COALESCE(v.variants_number, 0) AS variants_number, sa.quantity AS quantity, - COALESCE(f.is_favorite, 0) AS is_favorite + COALESCE(f.is_favorite, 0) AS is_favorite, + CASE + WHEN ps.date_add >= DATE_SUB( + NOW(), + INTERVAL ( + SELECT value + FROM ps_configuration + WHERE name = 'PS_NB_DAYS_NEW_PRODUCT' + ) DAY + ) AND ps.active = 1 + THEN 1 + ELSE 0 + END AS is_new `, 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). diff --git a/app/repos/searchRepo/searchRepo.go b/app/repos/searchRepo/searchRepo.go index de4d5ea..d8b56ba 100644 --- a/app/repos/searchRepo/searchRepo.go +++ b/app/repos/searchRepo/searchRepo.go @@ -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 +} diff --git a/app/service/meiliService/meiliService.go b/app/service/meiliService/meiliService.go index 6d9120a..41e9b8f 100644 --- a/app/service/meiliService/meiliService.go +++ b/app/service/meiliService/meiliService.go @@ -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) } diff --git a/app/utils/localeExtractor/localeExtractor.go b/app/utils/localeExtractor/localeExtractor.go index 37bdb0a..7dcd0cc 100644 --- a/app/utils/localeExtractor/localeExtractor.go +++ b/app/utils/localeExtractor/localeExtractor.go @@ -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 { diff --git a/i18n/migrations/20260302163123_create_tables_data.sql b/i18n/migrations/20260302163123_create_tables_data.sql index bb7fde3..5f7a634 100644 --- a/i18n/migrations/20260302163123_create_tables_data.sql +++ b/i18n/migrations/20260302163123_create_tables_data.sql @@ -35,15 +35,27 @@ 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 \ No newline at end of file From a0c3dd8ec8fe231a87c418a094c2207b7304e97a Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 14 Apr 2026 12:28:39 +0200 Subject: [PATCH 14/16] added filtering on is_new and is_favorite --- app/delivery/web/api/restricted/product.go | 16 +-- app/repos/productsRepo/productsRepo.go | 117 ++++++++++++--------- bruno/api_v1/product/Products List.yml | 13 ++- 3 files changed, 88 insertions(+), 58 deletions(-) diff --git a/app/delivery/web/api/restricted/product.go b/app/delivery/web/api/restricted/product.go index bc7778e..ba6b99d 100644 --- a/app/delivery/web/api/restricted/product.go +++ b/app/delivery/web/api/restricted/product.go @@ -103,15 +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", - "is_new": "is_new", + "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 { diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go index eae3f91..344b7a8 100644 --- a/app/repos/productsRepo/productsRepo.go +++ b/app/repos/productsRepo/productsRepo.go @@ -105,69 +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, - CASE - WHEN ps.date_add >= DATE_SUB( - NOW(), - INTERVAL ( - SELECT value - FROM ps_configuration - WHERE name = 'PS_NB_DAYS_NEW_PRODUCT' - ) DAY - ) AND ps.active = 1 - THEN 1 - ELSE 0 - END AS is_new - `, 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()...) diff --git a/bruno/api_v1/product/Products List.yml b/bruno/api_v1/product/Products List.yml index 01983e2..c099b28 100644 --- a/bruno/api_v1/product/Products List.yml +++ b/bruno/api_v1/product/Products List.yml @@ -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: "" From 1efc5417be729fa2b60c084b0368096687e19975 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 14 Apr 2026 12:32:24 +0200 Subject: [PATCH 15/16] permissions strings change --- app/delivery/middleware/perms/permissions.go | 2 +- .../web/api/restricted/productTranslation.go | 5 +++-- app/delivery/web/api/restricted/search.go | 3 ++- app/delivery/web/api/restricted/specificPrice.go | 15 ++++++++------- app/delivery/web/api/restricted/storage.go | 3 ++- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/delivery/middleware/perms/permissions.go b/app/delivery/middleware/perms/permissions.go index fb922d4..f30b097 100644 --- a/app/delivery/middleware/perms/permissions.go +++ b/app/delivery/middleware/perms/permissions.go @@ -8,7 +8,7 @@ const ( UserDeleteAny Permission = "user.delete.any" CurrencyWrite Permission = "currency.write" SpecificPriceManage Permission = "specific_price.manage" - CreateWebdavToken Permission = "webdav.create_token" + WebdavCreateToken Permission = "webdav.create_token" ProductTranslationSave Permission = "product_translation.save" ProductTranslationTranslate Permission = "product_translation.translate" SearchCreateIndex Permission = "search.create_index" diff --git a/app/delivery/web/api/restricted/productTranslation.go b/app/delivery/web/api/restricted/productTranslation.go index c995085..eb3fee1 100644 --- a/app/delivery/web/api/restricted/productTranslation.go +++ b/app/delivery/web/api/restricted/productTranslation.go @@ -5,6 +5,7 @@ import ( "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/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", middleware.Require("product_translation.save"), handler.SaveProductDescription) - r.Get("/translate-product-description", middleware.Require("product_translation.translate"), 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 } diff --git a/app/delivery/web/api/restricted/search.go b/app/delivery/web/api/restricted/search.go index dc79683..ebb3a76 100644 --- a/app/delivery/web/api/restricted/search.go +++ b/app/delivery/web/api/restricted/search.go @@ -5,6 +5,7 @@ import ( "fmt" "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", middleware.Require("search.create_index"), handler.CreateIndex) + r.Get("/create-index", middleware.Require(perms.SearchCreateIndex), handler.CreateIndex) r.Post("/search", handler.Search) r.Post("/settings", handler.GetSettings) diff --git a/app/delivery/web/api/restricted/specificPrice.go b/app/delivery/web/api/restricted/specificPrice.go index bece83d..e007f57 100644 --- a/app/delivery/web/api/restricted/specificPrice.go +++ b/app/delivery/web/api/restricted/specificPrice.go @@ -5,6 +5,7 @@ import ( "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" @@ -30,13 +31,13 @@ func NewSpecificPriceHandler() *SpecificPriceHandler { func SpecificPriceHandlerRoutes(r fiber.Router) fiber.Router { handler := NewSpecificPriceHandler() - r.Post("/", middleware.Require("specific_price.manage"), handler.Create) - r.Put("/:id", middleware.Require("specific_price.manage"), handler.Update) - r.Delete("/:id", middleware.Require("specific_price.manage"), handler.Delete) - r.Get("/", middleware.Require("specific_price.manage"), handler.List) - r.Get("/:id", middleware.Require("specific_price.manage"), handler.GetByID) - r.Patch("/:id/activate", middleware.Require("specific_price.manage"), handler.Activate) - r.Patch("/:id/deactivate", middleware.Require("specific_price.manage"), 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 } diff --git a/app/delivery/web/api/restricted/storage.go b/app/delivery/web/api/restricted/storage.go index 1bd631d..1a252b0 100644 --- a/app/delivery/web/api/restricted/storage.go +++ b/app/delivery/web/api/restricted/storage.go @@ -5,6 +5,7 @@ import ( "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/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", middleware.Require("webdav.create_token"), handler.CreateNewWebdavToken) + r.Get("/create-new-webdav-token", middleware.Require(perms.WebdavCreateToken), handler.CreateNewWebdavToken) return r } From c610ce38cca610d5cdd0ccd368aeeb94f8ed7ba8 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 14 Apr 2026 13:19:48 +0200 Subject: [PATCH 16/16] fixes after merging with main --- app/delivery/middleware/perms/permissions.go | 11 ++--------- app/service/orderService/orderService.go | 7 ++++--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/delivery/middleware/perms/permissions.go b/app/delivery/middleware/perms/permissions.go index 9887fa4..35183f3 100644 --- a/app/delivery/middleware/perms/permissions.go +++ b/app/delivery/middleware/perms/permissions.go @@ -3,14 +3,6 @@ package perms type Permission string const ( -<<<<<<< HEAD - UserReadAny Permission = "user.read.any" - UserWriteAny Permission = "user.write.any" - UserDeleteAny Permission = "user.delete.any" - CurrencyWrite Permission = "currency.write" - ViewAllOrders Permission = "orders.view" - ModifyAllOrders Permission = "orders.modify" -======= UserReadAny Permission = "user.read.any" UserWriteAny Permission = "user.write.any" UserDeleteAny Permission = "user.delete.any" @@ -20,5 +12,6 @@ const ( ProductTranslationSave Permission = "product_translation.save" ProductTranslationTranslate Permission = "product_translation.translate" SearchCreateIndex Permission = "search.create_index" ->>>>>>> e5988a85f32e0b3324f6d9f48254355fbffd559e + OrdersViewAll Permission = "orders.view_all" + OrdersModifyAll Permission = "orders.modify_all" ) diff --git a/app/service/orderService/orderService.go b/app/service/orderService/orderService.go index da21785..5e1a5ca 100644 --- a/app/service/orderService/orderService.go +++ b/app/service/orderService/orderService.go @@ -32,7 +32,7 @@ func New() *OrderService { } func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) { - if !user.HasPermission(perms.ViewAllOrders) { + if !user.HasPermission(perms.OrdersViewAll) { // append filter to view only this user's orders idStr := strconv.FormatUint(uint64(user.ID), 10) filt.Append(filters.Where("b2b_customer_orders.user_id = " + idStr)) @@ -114,7 +114,7 @@ func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, c return err } - if !user.HasPermission(perms.ModifyAllOrders) { + if !user.HasPermission(perms.OrdersModifyAll) { exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id) if err != nil { return err @@ -128,8 +128,9 @@ func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, c return s.ordersRepo.ChangeOrderAddress(order_id, country_id, address_info) } +// This is obiously just an initial version of this function func (s *OrderService) ChangeOrderStatus(user *model.Customer, order_id uint, status string) error { - if !user.HasPermission(perms.ModifyAllOrders) { + if !user.HasPermission(perms.OrdersModifyAll) { exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id) if err != nil { return err