carts #25

Merged
goc_marek merged 3 commits from carts into main 2026-03-25 01:32:50 +00:00
8 changed files with 303 additions and 13 deletions
Showing only changes of commit c464c02301 - Show all commits

View File

@@ -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)))
}

View File

@@ -110,6 +110,10 @@ func (s *Server) Setup() error {
meiliSearch := s.restricted.Group("/meili-search") meiliSearch := s.restricted.Group("/meili-search")
restricted.MeiliSearchHandlerRoutes(meiliSearch) restricted.MeiliSearchHandlerRoutes(meiliSearch)
// carts (restricted)
carts := s.restricted.Group("/carts")
restricted.CartsHandlerRoutes(carts)
// // Restricted routes example // // Restricted routes example
// restricted := s.api.Group("/restricted") // restricted := s.api.Group("/restricted")
// restricted.Use(middleware.AuthMiddleware()) // restricted.Use(middleware.AuthMiddleware())

24
app/model/cart.go Normal file
View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -3,3 +3,5 @@ package constdata
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads). // PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$` const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
const SHOP_ID = 1 const SHOP_ID = 1
const MAX_AMOUNT_OF_CARTS_PER_USER = 10
const DEFAULT_NEW_CART_NAME = "new cart"

View File

@@ -51,6 +51,10 @@ var (
// Typed errors for menu handler // Typed errors for menu handler
ErrNoRootFound = errors.New("no root found in categories table") ErrNoRootFound = errors.New("no root found in categories table")
// 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 // 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): case errors.Is(err, ErrNoRootFound):
return i18n.T_(c, "error.no_root_found") 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: default:
return i18n.T_(c, "error.err_internal_server_error") 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, ErrBadField),
errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrInvalidXHTML),
errors.Is(err, ErrBadPaging), errors.Is(err, ErrBadPaging),
errors.Is(err, ErrNoRootFound): errors.Is(err, ErrNoRootFound),
errors.Is(err, ErrMaxAmtOfCartsReached),
errors.Is(err, ErrUserHasNoSuchCart):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrEmailExists): case errors.Is(err, ErrEmailExists):
return fiber.StatusConflict return fiber.StatusConflict

View File

@@ -86,18 +86,27 @@ CREATE INDEX IF NOT EXISTS idx_customers_deleted_at
ON b2b_customers (deleted_at); ON b2b_customers (deleted_at);
-- customer_repo_accesses -- customer_carts
-- CREATE TABLE IF NOT EXISTS b2b_customer_repo_accesses ( CREATE TABLE IF NOT EXISTS b2b_customer_carts (
-- user_id BIGINT NOT NULL, cart_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
-- repo_id BIGINT NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
-- PRIMARY KEY (user_id, repo_id), name VARCHAR(255) NULL,
-- CONSTRAINT fk_customer_repo_user CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE
-- FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
-- ON DELETE CASCADE, CREATE INDEX IF NOT EXISTS idx_customer_carts_user_id ON b2b_customer_carts (user_id);
-- CONSTRAINT fk_customer_repo_repo
-- FOREIGN KEY (repo_id) REFERENCES repository(id)
-- ON DELETE CASCADE -- carts_products
-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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 -- refresh_tokens