From c464c023017fc2b258e3bf496a2d78b6032896d7 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Tue, 24 Mar 2026 14:46:38 +0100 Subject: [PATCH] add carts --- app/delivery/web/api/restricted/carts.go | 99 +++++++++++++++++++ app/delivery/web/init.go | 4 + app/model/cart.go | 24 +++++ app/repos/cartsRepo/cartsRepo.go | 82 +++++++++++++++ app/service/cartsService/cartsService.go | 59 +++++++++++ app/utils/const_data/consts.go | 2 + app/utils/responseErrors/responseErrors.go | 13 ++- .../20260302163122_create_tables.sql | 33 ++++--- 8 files changed, 303 insertions(+), 13 deletions(-) create mode 100644 app/delivery/web/api/restricted/carts.go create mode 100644 app/model/cart.go create mode 100644 app/repos/cartsRepo/cartsRepo.go create mode 100644 app/service/cartsService/cartsService.go diff --git a/app/delivery/web/api/restricted/carts.go b/app/delivery/web/api/restricted/carts.go new file mode 100644 index 0000000..7fd7a5f --- /dev/null +++ b/app/delivery/web/api/restricted/carts.go @@ -0,0 +1,99 @@ +package restricted + +import ( + "strconv" + + "git.ma-al.com/goc_daniel/b2b/app/service/cartsService" + "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" + "git.ma-al.com/goc_daniel/b2b/app/utils/response" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + "github.com/gofiber/fiber/v3" +) + +// CartsHandler handles endpoints that modify carts. +type CartsHandler struct { + cartsService *cartsService.CartsService +} + +// CartsHandler creates a new CartsHandler instance +func NewCartsHandler() *CartsHandler { + cartsService := cartsService.New() + return &CartsHandler{ + cartsService: cartsService, + } +} + +func CartsHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewCartsHandler() + + r.Get("/add-new-cart", handler.AddNewCart) + r.Get("/change-cart-name", handler.ChangeCartName) + r.Get("/retrieve-cart", handler.RetrieveCart) + + return r +} + +func (h *CartsHandler) AddNewCart(c fiber.Ctx) error { + userID, ok := c.Locals("userID").(uint) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + + new_cart, err := h.cartsService.CreateNewCart(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(&new_cart, 0, i18n.T_(c, response.Message_OK))) +} + +func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error { + userID, ok := c.Locals("userID").(uint) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) + } + + new_name := c.Query("new_name") + + 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))) + } + + err = h.cartsService.UpdateCartName(userID, uint(cart_id), new_name) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK))) +} + +func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error { + userID, ok := c.Locals("userID").(uint) + 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))) + } + + cart, err := h.cartsService.RetrieveCart(userID, uint(cart_id)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(cart, 0, i18n.T_(c, response.Message_OK))) +} diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index bc61539..9b332cd 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -110,6 +110,10 @@ func (s *Server) Setup() error { meiliSearch := s.restricted.Group("/meili-search") restricted.MeiliSearchHandlerRoutes(meiliSearch) + // carts (restricted) + carts := s.restricted.Group("/carts") + restricted.CartsHandlerRoutes(carts) + // // Restricted routes example // restricted := s.api.Group("/restricted") // restricted.Use(middleware.AuthMiddleware()) diff --git a/app/model/cart.go b/app/model/cart.go new file mode 100644 index 0000000..c7349be --- /dev/null +++ b/app/model/cart.go @@ -0,0 +1,24 @@ +package model + +type CustomerCart struct { + CartID uint64 `gorm:"column:cart_id;primaryKey;autoIncrement" json:"cart_id"` + UserID uint64 `gorm:"column:user_id;not null;index" json:"user_id"` + Name *string `gorm:"column:name;size:255" json:"name,omitempty"` + Products []CartProduct `gorm:"foreignKey:CartID;references:CartID" json:"products,omitempty"` +} + +func (CustomerCart) TableName() string { + return "b2b_customer_carts" +} + +type CartProduct struct { + ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + CartID uint64 `gorm:"column:cart_id;not null;index" json:"cart_id"` + ProductID uint `gorm:"column:product_id;not null" json:"product_id"` + ProductAttributeID *uint64 `gorm:"column:product_attribute_id" json:"product_attribute_id,omitempty"` + Amount int `gorm:"column:amount;not null" json:"amount"` +} + +func (CartProduct) TableName() string { + return "b2b_carts_products" +} diff --git a/app/repos/cartsRepo/cartsRepo.go b/app/repos/cartsRepo/cartsRepo.go new file mode 100644 index 0000000..5db3751 --- /dev/null +++ b/app/repos/cartsRepo/cartsRepo.go @@ -0,0 +1,82 @@ +package cartsRepo + +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" +) + +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) + UpdateCartName(user_id uint, cart_id uint, new_name string) error + RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) +} + +type CartsRepo struct{} + +func New() UICartsRepo { + return &CartsRepo{} +} + +func (repo *CartsRepo) CartsAmount(user_id uint) (uint, error) { + var amt uint + + err := db.DB. + Table("b2b_customer_carts"). + Select("COUNT(*) AS amt"). + Where("user_id = ?", user_id). + Scan(&amt). + Error + + return amt, err +} + +func (repo *CartsRepo) CreateNewCart(user_id uint) (model.CustomerCart, error) { + var name string + name = constdata.DEFAULT_NEW_CART_NAME + + cart := model.CustomerCart{ + UserID: uint64(user_id), + Name: &name, + } + err := db.DB.Create(&cart).Error + + return cart, err +} + +func (repo *CartsRepo) UserHasCart(user_id uint, cart_id uint) (uint, error) { + var amt uint + + err := db.DB. + Table("b2b_customer_carts"). + Select("COUNT(*) AS amt"). + Where("user_id = ? AND cart_id = ?", user_id, cart_id). + Scan(&amt). + Error + + return amt, err +} + +func (repo *CartsRepo) UpdateCartName(user_id uint, cart_id uint, new_name string) error { + err := db.DB. + Table("b2b_customer_carts"). + Where("user_id = ? AND cart_id = ?", user_id, cart_id). + Update("name", new_name). + Error + + return err +} + +func (repo *CartsRepo) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) { + var cart model.CustomerCart + + err := db.DB. + Preload("b2b_carts_products"). + Where("user_id = ? AND cart_id = ?", user_id, cart_id). + First(&cart). + Error + + return &cart, err +} diff --git a/app/service/cartsService/cartsService.go b/app/service/cartsService/cartsService.go new file mode 100644 index 0000000..7fe0568 --- /dev/null +++ b/app/service/cartsService/cartsService.go @@ -0,0 +1,59 @@ +package cartsService + +import ( + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" +) + +type CartsService struct { + repo cartsRepo.UICartsRepo +} + +func New() *CartsService { + return &CartsService{ + repo: cartsRepo.New(), + } +} + +func (s *CartsService) CreateNewCart(user_id uint) (model.CustomerCart, error) { + var cart model.CustomerCart + + customers_carts_amount, err := s.repo.CartsAmount(user_id) + if err != nil { + return cart, err + } + if customers_carts_amount >= constdata.MAX_AMOUNT_OF_CARTS_PER_USER { + return cart, responseErrors.ErrMaxAmtOfCartsReached + } + + // create new cart for customer + cart, err = s.repo.CreateNewCart(user_id) + + return cart, nil +} + +func (s *CartsService) UpdateCartName(user_id uint, cart_id uint, new_name string) error { + amt, err := s.repo.UserHasCart(user_id, cart_id) + if err != nil { + return err + } + if amt != 1 { + return responseErrors.ErrUserHasNoSuchCart + } + + return s.repo.UpdateCartName(user_id, cart_id, new_name) +} + +func (s *CartsService) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) { + amt, err := s.repo.UserHasCart(user_id, cart_id) + if err != nil { + return nil, err + } + if amt != 1 { + return nil, responseErrors.ErrUserHasNoSuchCart + } + + return s.repo.RetrieveCart(user_id, cart_id) +} diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index 8bdce27..e8fa951 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -3,3 +3,5 @@ package constdata // PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads). const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` const SHOP_ID = 1 +const MAX_AMOUNT_OF_CARTS_PER_USER = 10 +const DEFAULT_NEW_CART_NAME = "new cart" diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index fffa66b..b0c1172 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -51,6 +51,10 @@ var ( // Typed errors for menu handler ErrNoRootFound = errors.New("no root found in categories table") + + // Typed errors for carts handler + ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") + ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ) // Error represents an error with HTTP status code @@ -141,6 +145,11 @@ func GetErrorCode(c fiber.Ctx, err error) string { case errors.Is(err, ErrNoRootFound): return i18n.T_(c, "error.no_root_found") + case errors.Is(err, ErrMaxAmtOfCartsReached): + return i18n.T_(c, "error.max_amt_of_carts_reached") + case errors.Is(err, ErrUserHasNoSuchCart): + return i18n.T_(c, "error.max_amt_of_carts_reached") + default: return i18n.T_(c, "error.err_internal_server_error") } @@ -176,7 +185,9 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrBadField), errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrBadPaging), - errors.Is(err, ErrNoRootFound): + errors.Is(err, ErrNoRootFound), + errors.Is(err, ErrMaxAmtOfCartsReached), + errors.Is(err, ErrUserHasNoSuchCart): return fiber.StatusBadRequest case errors.Is(err, ErrEmailExists): return fiber.StatusConflict diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index 6113dfe..903e36a 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -86,18 +86,27 @@ CREATE INDEX IF NOT EXISTS idx_customers_deleted_at ON b2b_customers (deleted_at); --- customer_repo_accesses --- CREATE TABLE IF NOT EXISTS b2b_customer_repo_accesses ( --- user_id BIGINT NOT NULL, --- repo_id BIGINT NOT NULL, --- PRIMARY KEY (user_id, repo_id), --- CONSTRAINT fk_customer_repo_user --- FOREIGN KEY (user_id) REFERENCES b2b_customers(id) --- ON DELETE CASCADE, --- CONSTRAINT fk_customer_repo_repo --- FOREIGN KEY (repo_id) REFERENCES repository(id) --- ON DELETE CASCADE --- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +-- customer_carts +CREATE TABLE IF NOT EXISTS b2b_customer_carts ( + 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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; +CREATE INDEX IF NOT EXISTS idx_customer_carts_user_id ON b2b_customer_carts (user_id); + + +-- carts_products +CREATE TABLE IF NOT EXISTS b2b_carts_products ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + cart_id BIGINT UNSIGNED NOT NULL, + product_id INT UNSIGNED NOT NULL, + product_attribute_id BIGINT NULL, + amount INT NOT NULL, + CONSTRAINT fk_carts_products_customer_carts FOREIGN KEY (cart_id) REFERENCES b2b_customer_carts (cart_id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_carts_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_carts_products_cart_id ON b2b_carts_products (cart_id); -- refresh_tokens