diff --git a/app/delivery/web/api/restricted/carts.go b/app/delivery/web/api/restricted/carts.go new file mode 100644 index 0000000..aeed1ee --- /dev/null +++ b/app/delivery/web/api/restricted/carts.go @@ -0,0 +1,168 @@ +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-carts-info", handler.RetrieveCartsInfo) + r.Get("/retrieve-cart", handler.RetrieveCart) + r.Get("/add-product-to-cart", handler.AddProduct) + + 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) RetrieveCartsInfo(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))) + } + + carts_info, err := h.cartsService.RetrieveCartsInfo(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(&carts_info, 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))) +} + +func (h *CartsHandler) AddProduct(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))) + } + + product_id_attribute := c.Query("product_id") + product_id, err := strconv.Atoi(product_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + product_attribute_id_attribute := c.Query("product_attribute_id") + var product_attribute_id *uint + if product_attribute_id_attribute == "" { + product_attribute_id = nil + } else { + val, err := strconv.Atoi(product_attribute_id_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + uval := uint(val) + product_attribute_id = &uval + } + + amount_attribute := c.Query("amount") + amount, err := strconv.Atoi(amount_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.AddProduct(userID, uint(cart_id), uint(product_id), product_attribute_id, uint(amount)) + 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 c18aaf0..563f47f 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) + s.api.All("*", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusNotFound) }) diff --git a/app/model/cart.go b/app/model/cart.go new file mode 100644 index 0000000..7e30f0f --- /dev/null +++ b/app/model/cart.go @@ -0,0 +1,24 @@ +package model + +type CustomerCart struct { + CartID uint `gorm:"column:cart_id;primaryKey;autoIncrement" json:"cart_id"` + UserID uint `gorm:"column:user_id;not null;index" json:"-"` + 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 uint `gorm:"column:id;primaryKey;autoIncrement" json:"-"` + CartID uint `gorm:"column:cart_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 (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..b15700c --- /dev/null +++ b/app/repos/cartsRepo/cartsRepo.go @@ -0,0 +1,133 @@ +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 + 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) + AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) 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: 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) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) { + var carts []model.CustomerCart + + err := db.DB. + Table("b2b_customer_carts"). + Where("user_id = ?", user_id). + Scan(&carts). + Error + + return carts, err +} + +func (repo *CartsRepo) RetrieveCart(user_id uint, cart_id uint) (*model.CustomerCart, error) { + var cart model.CustomerCart + + err := db.DB. + Preload("Products"). + Where("user_id = ? AND cart_id = ?", user_id, cart_id). + First(&cart). + Error + + return &cart, err +} + +func (repo *CartsRepo) CheckProductExists(product_id uint, product_attribute_id *uint) (uint, error) { + var amt uint + + if product_attribute_id == nil { + err := db.DB. + Table("ps_product_shop"). + Select("COUNT(*) AS amt"). + Where("id_product = ?", product_id). + Scan(&amt). + Error + return amt, err + + } else { + err := db.DB. + Table("ps_product_shop AS ps"). + Joins("INNER JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product"). + Select("COUNT(*) AS amt"). + Where("ps.id_product = ? AND pas.id_product_attribute = ?", product_id, *product_attribute_id). + Scan(&amt). + Error + return amt, err + } +} + +func (repo *CartsRepo) AddProduct(user_id uint, cart_id uint, product_id uint, product_attribute_id *uint, amount uint) error { + product := model.CartProduct{ + CartID: cart_id, + ProductID: product_id, + ProductAttributeID: product_attribute_id, + Amount: amount, + } + err := db.DB.Create(&product).Error + + return err +} diff --git a/app/service/cartsService/cartsService.go b/app/service/cartsService/cartsService.go new file mode 100644 index 0000000..c82e7b2 --- /dev/null +++ b/app/service/cartsService/cartsService.go @@ -0,0 +1,83 @@ +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) RetrieveCartsInfo(user_id uint) ([]model.CustomerCart, error) { + return s.repo.RetrieveCartsInfo(user_id) +} + +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) +} + +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) + if err != nil { + return err + } + if amt != 1 { + return responseErrors.ErrUserHasNoSuchCart + } + + amt, err = s.repo.CheckProductExists(product_id, product_attribute_id) + if err != nil { + return err + } + if amt != 1 { + return responseErrors.ErrProductOrItsVariationDoesNotExist + } + + return s.repo.AddProduct(user_id, cart_id, product_id, product_attribute_id, amount) +} 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..f658430 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -51,6 +51,11 @@ 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") + ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") ) // Error represents an error with HTTP status code @@ -141,6 +146,13 @@ 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.user_has_no_such_cart") + case errors.Is(err, ErrProductOrItsVariationDoesNotExist): + return i18n.T_(c, "error.product_or_its_variation_does_not_exist") + default: return i18n.T_(c, "error.err_internal_server_error") } @@ -176,7 +188,10 @@ 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), + errors.Is(err, ErrProductOrItsVariationDoesNotExist): 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..c2e104b 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 UNSIGNED 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