From b7a1d10898d121ebccd054cc8c64dd4b9f9f00f6 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 22 May 2025 01:42:14 +0700 Subject: [PATCH] fix: fixing cart feature behavior and auto commit optimizing --- cmd/main.go | 18 +- dto/trashcart_dto.go | 72 +++--- go.mod | 1 + go.sum | 2 + internal/handler/cart_handler.go | 114 +++++++++ internal/handler/trashcart_handler.go | 96 ------- internal/services/cart_redis.go | 104 ++++++++ internal/services/cart_service.go | 35 +++ internal/services/trashcart_redisservices.go | 124 --------- internal/services/trashcart_service.go | 254 +++++++++---------- internal/worker/cart_worker.go | 141 ++++++---- model/trashcart_model.go | 18 +- presentation/cart_router.go | 24 ++ presentation/trashcart_route.go | 27 -- 14 files changed, 550 insertions(+), 480 deletions(-) create mode 100644 internal/handler/cart_handler.go delete mode 100644 internal/handler/trashcart_handler.go create mode 100644 internal/services/cart_redis.go create mode 100644 internal/services/cart_service.go delete mode 100644 internal/services/trashcart_redisservices.go create mode 100644 presentation/cart_router.go delete mode 100644 presentation/trashcart_route.go diff --git a/cmd/main.go b/cmd/main.go index 2ab9825..e99d0be 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,19 +1,33 @@ package main import ( + "log" + "os" "rijig/config" + "rijig/internal/worker" "rijig/router" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/robfig/cron" ) func main() { config.SetupConfig() + logFile, _ := os.OpenFile("logs/cart_commit.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + log.SetOutput(logFile) + + go func() { + c := cron.New() + c.AddFunc("@every 1m", func() { + _ = worker.CommitExpiredCartsToDB() + }) + c.Start() + }() app := fiber.New() app.Use(cors.New(cors.Config{ - AllowOrigins: "*", + AllowOrigins: "*", AllowMethods: "GET,POST,PUT,PATCH,DELETE", AllowHeaders: "Content-Type,x-api-key", })) @@ -43,4 +57,4 @@ func main() { router.SetupRoutes(app) config.StartServer(app) -} \ No newline at end of file +} diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go index 3f4a541..fe124fa 100644 --- a/dto/trashcart_dto.go +++ b/dto/trashcart_dto.go @@ -5,65 +5,53 @@ import ( "strings" ) -type ValidationErrors struct { - Errors map[string][]string +type RequestCartItemDTO struct { + TrashID string `json:"trash_id"` + Amount float32 `json:"amount"` } -func (v ValidationErrors) Error() string { - return "validation error" +type RequestCartDTO struct { + CartItems []RequestCartItemDTO `json:"cart_items"` } -type CartResponse struct { - ID string `json:"id"` - UserID string `json:"userid"` - CartItems []CartItemResponse `json:"cartitems"` - TotalAmount float32 `json:"totalamount"` - EstimatedTotalPrice float32 `json:"estimated_totalprice"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -type CartItemResponse struct { - ItemId string `json:"item_id"` - TrashId string `json:"trashid"` - TrashIcon string `json:"trashicon"` - TrashName string `json:"trashname"` +type ResponseCartItemDTO struct { + ID string `json:"id"` + TrashID string `json:"trash_id"` + TrashName string `json:"trash_name"` + TrashIcon string `json:"trash_icon"` Amount float32 `json:"amount"` - EstimatedSubTotalPrice float32 `json:"estimated_subtotalprice"` + SubTotalEstimatedPrice float32 `json:"subtotal_estimated_price"` } -type RequestCartItems struct { - TrashCategoryID string `json:"trashid"` - Amount float32 `json:"amount"` +type ResponseCartDTO struct { + ID string `json:"id"` + UserID string `json:"user_id"` + TotalAmount float32 `json:"total_amount"` + EstimatedTotalPrice float32 `json:"estimated_total_price"` + CartItems []ResponseCartItemDTO `json:"cart_items"` } -func (r *RequestCartItems) ValidateRequestCartItem() (map[string][]string, bool) { +// ==== VALIDATION ==== + +func (r *RequestCartDTO) ValidateRequestCartDTO() (map[string][]string, bool) { errors := make(map[string][]string) - if strings.TrimSpace(r.TrashCategoryID) == "" { - errors["trashid"] = append(errors["trashid"], "trashid is required") + if len(r.CartItems) == 0 { + errors["cart_items"] = append(errors["cart_items"], "minimal satu item harus dimasukkan") } - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -type BulkRequestCartItems struct { - Items []RequestCartItems `json:"items"` -} - -func (b *BulkRequestCartItems) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - for i, item := range b.Items { - if strings.TrimSpace(item.TrashCategoryID) == "" { - errors[fmt.Sprintf("items[%d].trashid", i)] = append(errors[fmt.Sprintf("items[%d].trashid", i)], "trashid is required") + for i, item := range r.CartItems { + if strings.TrimSpace(item.TrashID) == "" { + errors[fmt.Sprintf("cart_items[%d].trash_id", i)] = append(errors[fmt.Sprintf("cart_items[%d].trash_id", i)], "trash_id tidak boleh kosong") + } + if item.Amount <= 0 { + errors[fmt.Sprintf("cart_items[%d].amount", i)] = append(errors[fmt.Sprintf("cart_items[%d].amount", i)], "amount harus lebih dari 0") } } + if len(errors) > 0 { return errors, false } + return nil, true } diff --git a/go.mod b/go.mod index 687dde0..93ed566 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect + github.com/robfig/cron v1.2.0 github.com/rs/zerolog v1.33.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect diff --git a/go.sum b/go.sum index d5b6c0b..a238c19 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= diff --git a/internal/handler/cart_handler.go b/internal/handler/cart_handler.go new file mode 100644 index 0000000..ab98fd7 --- /dev/null +++ b/internal/handler/cart_handler.go @@ -0,0 +1,114 @@ +package handler + +import ( + "context" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CartHandler interface { + GetCart(c *fiber.Ctx) error + AddOrUpdateCartItem(c *fiber.Ctx) error + AddMultipleCartItems(c *fiber.Ctx) error + DeleteCartItem(c *fiber.Ctx) error + ClearCart(c *fiber.Ctx) error +} + +type cartHandler struct { + service services.CartService +} + +func NewCartHandler(service services.CartService) CartHandler { + return &cartHandler{service: service} +} + +// GET /cart +func (h *cartHandler) GetCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + cart, err := h.service.GetCart(context.Background(), userID) + if err != nil { + return utils.ErrorResponse(c, "Cart belum dibuat atau sudah kadaluarsa") + } + + return utils.SuccessResponse(c, cart, "Data cart berhasil diambil") +} + +// POST /cart/item +func (h *cartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + var item dto.RequestCartItemDTO + if err := c.BodyParser(&item); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"format tidak valid"}}) + } + + if item.TrashID == "" || item.Amount <= 0 { + return utils.ValidationErrorResponse(c, map[string][]string{ + "trash_id": {"harus diisi"}, + "amount": {"harus lebih dari 0"}, + }) + } + + if err := h.service.AddOrUpdateItem(context.Background(), userID, item); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Item berhasil ditambahkan/diupdate di cart") +} + +// POST /cart/items +func (h *cartHandler) AddMultipleCartItems(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + var payload dto.RequestCartDTO + if err := c.BodyParser(&payload); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format tidak valid"}, + }) + } + + if errs, ok := payload.ValidateRequestCartDTO(); !ok { + return utils.ValidationErrorResponse(c, errs) + } + + for _, item := range payload.CartItems { + if err := h.service.AddOrUpdateItem(context.Background(), userID, item); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + } + + return utils.SuccessResponse(c, nil, "Semua item berhasil ditambahkan/diupdate ke cart") +} + + +// DELETE /cart/item/:trashID +func (h *cartHandler) DeleteCartItem(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + trashID := c.Params("trashID") + + if trashID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{"trash_id": {"tidak boleh kosong"}}) + } + + err := h.service.DeleteItem(context.Background(), userID, trashID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Item berhasil dihapus dari cart") +} + +// DELETE /cart +func (h *cartHandler) ClearCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + if err := h.service.ClearCart(context.Background(), userID); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Seluruh cart berhasil dihapus") +} diff --git a/internal/handler/trashcart_handler.go b/internal/handler/trashcart_handler.go deleted file mode 100644 index 7890e77..0000000 --- a/internal/handler/trashcart_handler.go +++ /dev/null @@ -1,96 +0,0 @@ -package handler - -import ( - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type CartHandler struct { - Service *services.CartService -} - -func NewCartHandler(service *services.CartService) *CartHandler { - return &CartHandler{Service: service} -} - -func (h *CartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error { - var body dto.BulkRequestCartItems - if err := c.BodyParser(&body); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"Invalid JSON body"}, - }) - } - - if errors, ok := body.Validate(); !ok { - return utils.ValidationErrorResponse(c, errors) - } - - userID := c.Locals("userID").(string) - for _, item := range body.Items { - if err := services.AddOrUpdateCartItem(userID, item); err != nil { - return utils.InternalServerErrorResponse(c, "Failed to update one or more items") - } - } - - return utils.SuccessResponse(c, nil, "Cart updated successfully") -} - -func (h *CartHandler) DeleteCartItem(c *fiber.Ctx) error { - trashID := c.Params("trashid") - userID := c.Locals("userID").(string) - - err := services.DeleteCartItem(userID, trashID) - if err != nil { - if err.Error() == "no cart found" || err.Error() == "trashid not found" { - return utils.GenericResponse(c, fiber.StatusNotFound, "Trash item not found in cart") - } - return utils.InternalServerErrorResponse(c, "Failed to delete item") - } - - return utils.SuccessResponse(c, nil, "Item deleted") -} - -func (h *CartHandler) ClearCart(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - if err := services.ClearCart(userID); err != nil { - return utils.InternalServerErrorResponse(c, "Failed to clear cart") - } - return utils.SuccessResponse(c, nil, "Cart cleared") -} - -func (h *CartHandler) GetCart(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - cart, err := h.Service.GetCart(userID) - if err != nil { - return utils.InternalServerErrorResponse(c, "Failed to fetch cart") - } - - return utils.SuccessResponse(c, cart, "User cart data successfully fetched") -} - -func (h *CartHandler) CommitCart(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - err := h.Service.CommitCartToDatabase(userID) - if err != nil { - return utils.InternalServerErrorResponse(c, "Failed to commit cart to database") - } - - return utils.SuccessResponse(c, nil, "Cart committed to database") -} - -// PUT /cart/refresh → refresh TTL Redis -func (h *CartHandler) RefreshCartTTL(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - err := services.RefreshCartTTL(userID) - if err != nil { - return utils.InternalServerErrorResponse(c, "Failed to refresh cart TTL") - } - - return utils.SuccessResponse(c, nil, "Cart TTL refreshed") -} diff --git a/internal/services/cart_redis.go b/internal/services/cart_redis.go new file mode 100644 index 0000000..aff0ce9 --- /dev/null +++ b/internal/services/cart_redis.go @@ -0,0 +1,104 @@ +package services + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "rijig/config" + "rijig/dto" +) + +var cartTTL = 30 * time.Minute + +func getCartKey(userID string) string { + return fmt.Sprintf("cart:user:%s", userID) +} + +func SetCartToRedis(ctx context.Context, userID string, cart dto.RequestCartDTO) error { + key := getCartKey(userID) + + data, err := json.Marshal(cart) + if err != nil { + return fmt.Errorf("failed to marshal cart: %w", err) + } + + err = config.RedisClient.Set(ctx, key, data, cartTTL).Err() + if err != nil { + return fmt.Errorf("failed to save cart to redis: %w", err) + } + + return nil +} + +func GetCartFromRedis(ctx context.Context, userID string) (*dto.RequestCartDTO, error) { + key := getCartKey(userID) + val, err := config.RedisClient.Get(ctx, key).Result() + if err != nil { + return nil, err + } + + var cart dto.RequestCartDTO + if err := json.Unmarshal([]byte(val), &cart); err != nil { + return nil, fmt.Errorf("failed to unmarshal cart data: %w", err) + } + + return &cart, nil +} + +func DeleteCartFromRedis(ctx context.Context, userID string) error { + key := getCartKey(userID) + return config.RedisClient.Del(ctx, key).Err() +} + +func GetCartTTL(ctx context.Context, userID string) (time.Duration, error) { + key := getCartKey(userID) + return config.RedisClient.TTL(ctx, key).Result() +} + +func UpdateOrAddCartItemToRedis(ctx context.Context, userID string, item dto.RequestCartItemDTO) error { + cart, err := GetCartFromRedis(ctx, userID) + if err != nil { + + cart = &dto.RequestCartDTO{ + CartItems: []dto.RequestCartItemDTO{item}, + } + return SetCartToRedis(ctx, userID, *cart) + } + + updated := false + for i, ci := range cart.CartItems { + if ci.TrashID == item.TrashID { + cart.CartItems[i].Amount = item.Amount + updated = true + break + } + } + if !updated { + cart.CartItems = append(cart.CartItems, item) + } + + return SetCartToRedis(ctx, userID, *cart) +} + +func RemoveCartItemFromRedis(ctx context.Context, userID, trashID string) error { + cart, err := GetCartFromRedis(ctx, userID) + if err != nil { + return err + } + + updatedItems := make([]dto.RequestCartItemDTO, 0) + for _, ci := range cart.CartItems { + if ci.TrashID != trashID { + updatedItems = append(updatedItems, ci) + } + } + + if len(updatedItems) == 0 { + return DeleteCartFromRedis(ctx, userID) + } + + cart.CartItems = updatedItems + return SetCartToRedis(ctx, userID, *cart) +} diff --git a/internal/services/cart_service.go b/internal/services/cart_service.go new file mode 100644 index 0000000..d98cd8d --- /dev/null +++ b/internal/services/cart_service.go @@ -0,0 +1,35 @@ +package services + +import ( + "rijig/dto" + "context" +) + +type CartService interface { + GetCart(ctx context.Context, userID string) (*dto.RequestCartDTO, error) + AddOrUpdateItem(ctx context.Context, userID string, item dto.RequestCartItemDTO) error + DeleteItem(ctx context.Context, userID string, trashID string) error + ClearCart(ctx context.Context, userID string) error +} + +type cartService struct{} + +func NewCartService() CartService { + return &cartService{} +} + +func (s *cartService) GetCart(ctx context.Context, userID string) (*dto.RequestCartDTO, error) { + return GetCartFromRedis(ctx, userID) +} + +func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, item dto.RequestCartItemDTO) error { + return UpdateOrAddCartItemToRedis(ctx, userID, item) +} + +func (s *cartService) DeleteItem(ctx context.Context, userID string, trashID string) error { + return RemoveCartItemFromRedis(ctx, userID, trashID) +} + +func (s *cartService) ClearCart(ctx context.Context, userID string) error { + return DeleteCartFromRedis(ctx, userID) +} \ No newline at end of file diff --git a/internal/services/trashcart_redisservices.go b/internal/services/trashcart_redisservices.go deleted file mode 100644 index 12b1a12..0000000 --- a/internal/services/trashcart_redisservices.go +++ /dev/null @@ -1,124 +0,0 @@ -package services - -import ( - "encoding/json" - "fmt" - "log" - "time" - - "rijig/config" - "rijig/dto" - - "github.com/go-redis/redis/v8" -) - -const cartTTL = 1 * time.Minute - -func getCartKey(userID string) string { - return fmt.Sprintf("cart:%s", userID) -} - -func GetCartItems(userID string) ([]dto.RequestCartItems, error) { - key := getCartKey(userID) - val, err := config.RedisClient.Get(config.Ctx, key).Result() - if err != nil { - return nil, err - } - - var items []dto.RequestCartItems - err = json.Unmarshal([]byte(val), &items) - if err != nil { - return nil, err - } - - return items, nil -} - -func AddOrUpdateCartItem(userID string, newItem dto.RequestCartItems) error { - key := getCartKey(userID) - var cartItems []dto.RequestCartItems - - val, err := config.RedisClient.Get(config.Ctx, key).Result() - if err == nil && val != "" { - json.Unmarshal([]byte(val), &cartItems) - } - - updated := false - for i, item := range cartItems { - if item.TrashCategoryID == newItem.TrashCategoryID { - if newItem.Amount == 0 { - cartItems = append(cartItems[:i], cartItems[i+1:]...) - } else { - cartItems[i].Amount = newItem.Amount - } - updated = true - break - } - } - - if !updated && newItem.Amount > 0 { - cartItems = append(cartItems, newItem) - } - - return setCartItems(key, cartItems) -} - -func DeleteCartItem(userID, trashID string) error { - key := fmt.Sprintf("cart:%s", userID) - items, err := GetCartItems(userID) - - if err == redis.Nil { - - log.Printf("No cart found in Redis for user: %s", userID) - return fmt.Errorf("no cart found") - } - - if err != nil { - log.Printf("Redis error: %v", err) - return err - } - - index := -1 - for i, item := range items { - if item.TrashCategoryID == trashID { - index = i - break - } - } - - if index == -1 { - log.Printf("TrashCategoryID %s not found in cart for user %s", trashID, userID) - return fmt.Errorf("trashid not found") - } - - items = append(items[:index], items[index+1:]...) - - if len(items) == 0 { - return config.RedisClient.Del(config.Ctx, key).Err() - } - - return setCartItems(key, items) -} - -func ClearCart(userID string) error { - key := getCartKey(userID) - return config.RedisClient.Del(config.Ctx, key).Err() -} - -func RefreshCartTTL(userID string) error { - key := getCartKey(userID) - return config.RedisClient.Expire(config.Ctx, key, cartTTL).Err() -} - -func setCartItems(key string, items []dto.RequestCartItems) error { - data, err := json.Marshal(items) - if err != nil { - return err - } - - err = config.RedisClient.Set(config.Ctx, key, data, cartTTL).Err() - if err != nil { - log.Printf("Redis SetCart error: %v", err) - } - return err -} diff --git a/internal/services/trashcart_service.go b/internal/services/trashcart_service.go index b8442a6..a231c47 100644 --- a/internal/services/trashcart_service.go +++ b/internal/services/trashcart_service.go @@ -1,154 +1,154 @@ package services -import ( - "log" - "time" +// import ( +// "log" +// "time" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" +// "rijig/dto" +// "rijig/internal/repositories" +// "rijig/model" - "github.com/google/uuid" -) +// "github.com/google/uuid" +// ) -type CartService struct { - Repo repositories.CartRepository -} +// type CartService struct { +// Repo repositories.CartRepository +// } -func NewCartService(repo repositories.CartRepository) *CartService { - return &CartService{Repo: repo} -} +// func NewCartService(repo repositories.CartRepository) *CartService { +// return &CartService{Repo: repo} +// } -func (s *CartService) CommitCartToDatabase(userID string) error { - items, err := GetCartItems(userID) - if err != nil || len(items) == 0 { - log.Printf("No items to commit for user: %s", userID) - return err - } +// func (s *CartService) CommitCartToDatabase(userID string) error { +// items, err := GetCartItems(userID) +// if err != nil || len(items) == 0 { +// log.Printf("No items to commit for user: %s", userID) +// return err +// } - var cartItems []model.CartItem - var totalAmount float32 - var estimatedTotal float32 +// var cartItems []model.CartItem +// var totalAmount float32 +// var estimatedTotal float32 - for _, item := range items { - trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) - if err != nil { - log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID) - continue - } +// for _, item := range items { +// trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) +// if err != nil { +// log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID) +// continue +// } - subTotal := float32(trash.EstimatedPrice) * item.Amount - totalAmount += item.Amount - estimatedTotal += subTotal +// subTotal := float32(trash.EstimatedPrice) * item.Amount +// totalAmount += item.Amount +// estimatedTotal += subTotal - cartItems = append(cartItems, model.CartItem{ - ID: uuid.NewString(), - TrashCategoryID: item.TrashCategoryID, - Amount: item.Amount, - SubTotalEstimatedPrice: subTotal, - }) - } +// cartItems = append(cartItems, model.CartItem{ +// ID: uuid.NewString(), +// TrashCategoryID: item.TrashCategoryID, +// Amount: item.Amount, +// SubTotalEstimatedPrice: subTotal, +// }) +// } - cart := &model.Cart{ - ID: uuid.NewString(), - UserID: userID, - CartItems: cartItems, - TotalAmount: totalAmount, - EstimatedTotalPrice: estimatedTotal, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } +// cart := &model.Cart{ +// ID: uuid.NewString(), +// UserID: userID, +// CartItems: cartItems, +// TotalAmount: totalAmount, +// EstimatedTotalPrice: estimatedTotal, +// CreatedAt: time.Now(), +// UpdatedAt: time.Now(), +// } - if err := s.Repo.DeleteCartByUserID(userID); err != nil { - log.Printf("Failed to delete old cart: %v", err) - } +// if err := s.Repo.DeleteCartByUserID(userID); err != nil { +// log.Printf("Failed to delete old cart: %v", err) +// } - if err := s.Repo.CreateCart(cart); err != nil { - log.Printf("Failed to create cart: %v", err) - return err - } +// if err := s.Repo.CreateCart(cart); err != nil { +// log.Printf("Failed to create cart: %v", err) +// return err +// } - if err := ClearCart(userID); err != nil { - log.Printf("Failed to clear Redis cart: %v", err) - } +// if err := ClearCart(userID); err != nil { +// log.Printf("Failed to clear Redis cart: %v", err) +// } - log.Printf("Cart committed successfully for user: %s", userID) - return nil -} +// log.Printf("Cart committed successfully for user: %s", userID) +// return nil +// } -func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) { - items, err := GetCartItems(userID) - if err != nil || len(items) == 0 { - return nil, err - } +// func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) { +// items, err := GetCartItems(userID) +// if err != nil || len(items) == 0 { +// return nil, err +// } - var totalAmount float32 - var estimatedTotal float32 - var cartItemDTOs []dto.CartItemResponse +// var totalAmount float32 +// var estimatedTotal float32 +// var cartItemDTOs []dto.CartItemResponse - for _, item := range items { - trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) - if err != nil { - continue - } +// for _, item := range items { +// trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) +// if err != nil { +// continue +// } - subtotal := float32(trash.EstimatedPrice) * item.Amount - totalAmount += item.Amount - estimatedTotal += subtotal +// subtotal := float32(trash.EstimatedPrice) * item.Amount +// totalAmount += item.Amount +// estimatedTotal += subtotal - cartItemDTOs = append(cartItemDTOs, dto.CartItemResponse{ - TrashId: trash.ID, - TrashIcon: trash.Icon, - TrashName: trash.Name, - Amount: item.Amount, - EstimatedSubTotalPrice: subtotal, - }) - } +// cartItemDTOs = append(cartItemDTOs, dto.CartItemResponse{ +// TrashId: trash.ID, +// TrashIcon: trash.Icon, +// TrashName: trash.Name, +// Amount: item.Amount, +// EstimatedSubTotalPrice: subtotal, +// }) +// } - resp := &dto.CartResponse{ - ID: "N/A", - UserID: userID, - TotalAmount: totalAmount, - EstimatedTotalPrice: estimatedTotal, - CreatedAt: time.Now().Format(time.RFC3339), - UpdatedAt: time.Now().Format(time.RFC3339), - CartItems: cartItemDTOs, - } - return resp, nil -} +// resp := &dto.CartResponse{ +// ID: "N/A", +// UserID: userID, +// TotalAmount: totalAmount, +// EstimatedTotalPrice: estimatedTotal, +// CreatedAt: time.Now().Format(time.RFC3339), +// UpdatedAt: time.Now().Format(time.RFC3339), +// CartItems: cartItemDTOs, +// } +// return resp, nil +// } -func (s *CartService) GetCart(userID string) (*dto.CartResponse, error) { +// func (s *CartService) GetCart(userID string) (*dto.CartResponse, error) { - cartRedis, err := s.GetCartFromRedis(userID) - if err == nil && len(cartRedis.CartItems) > 0 { - return cartRedis, nil - } +// cartRedis, err := s.GetCartFromRedis(userID) +// if err == nil && len(cartRedis.CartItems) > 0 { +// return cartRedis, nil +// } - cartDB, err := s.Repo.GetCartByUserID(userID) - if err != nil { - return nil, err - } +// cartDB, err := s.Repo.GetCartByUserID(userID) +// if err != nil { +// return nil, err +// } - var items []dto.CartItemResponse - for _, item := range cartDB.CartItems { - items = append(items, dto.CartItemResponse{ - ItemId: item.ID, - TrashId: item.TrashCategoryID, - TrashIcon: item.TrashCategory.Icon, - TrashName: item.TrashCategory.Name, - Amount: item.Amount, - EstimatedSubTotalPrice: item.SubTotalEstimatedPrice, - }) - } +// var items []dto.CartItemResponse +// for _, item := range cartDB.CartItems { +// items = append(items, dto.CartItemResponse{ +// ItemId: item.ID, +// TrashId: item.TrashCategoryID, +// TrashIcon: item.TrashCategory.Icon, +// TrashName: item.TrashCategory.Name, +// Amount: item.Amount, +// EstimatedSubTotalPrice: item.SubTotalEstimatedPrice, +// }) +// } - resp := &dto.CartResponse{ - ID: cartDB.ID, - UserID: cartDB.UserID, - TotalAmount: cartDB.TotalAmount, - EstimatedTotalPrice: cartDB.EstimatedTotalPrice, - CreatedAt: cartDB.CreatedAt.Format(time.RFC3339), - UpdatedAt: cartDB.UpdatedAt.Format(time.RFC3339), - CartItems: items, - } - return resp, nil -} +// resp := &dto.CartResponse{ +// ID: cartDB.ID, +// UserID: cartDB.UserID, +// TotalAmount: cartDB.TotalAmount, +// EstimatedTotalPrice: cartDB.EstimatedTotalPrice, +// CreatedAt: cartDB.CreatedAt.Format(time.RFC3339), +// UpdatedAt: cartDB.UpdatedAt.Format(time.RFC3339), +// CartItems: items, +// } +// return resp, nil +// } diff --git a/internal/worker/cart_worker.go b/internal/worker/cart_worker.go index ce208ab..07449fb 100644 --- a/internal/worker/cart_worker.go +++ b/internal/worker/cart_worker.go @@ -1,76 +1,111 @@ package worker import ( - "log" + "context" + "encoding/json" + "fmt" "strings" "time" "rijig/config" - "rijig/internal/services" + "rijig/dto" + "rijig/model" ) -const ( - lockPrefix = "lock:cart:" - lockExpiration = 30 * time.Second - commitThreshold = 20 * time.Second - scanPattern = "cart:*" -) +func CommitExpiredCartsToDB() error { + ctx := context.Background() -func StartCartCommitWorker(service *services.CartService) { + keys, err := config.RedisClient.Keys(ctx, "cart:user:*").Result() + if err != nil { + return fmt.Errorf("error fetching cart keys: %w", err) + } - go func() { - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - log.Println("🛠️ Cart Worker is running in background...") - for range ticker.C { - processCarts(service) - } - }() - -} - -func processCarts(service *services.CartService) { - iter := config.RedisClient.Scan(config.Ctx, 0, scanPattern, 0).Iterator() - for iter.Next(config.Ctx) { - key := iter.Val() - - ttl, err := config.RedisClient.TTL(config.Ctx, key).Result() - if err != nil { - log.Printf("❌ Error getting TTL for %s: %v", key, err) + for _, key := range keys { + ttl, err := config.RedisClient.TTL(ctx, key).Result() + if err != nil || ttl > 30*time.Second { continue } - if ttl > 0 && ttl < commitThreshold { - userID := extractUserIDFromKey(key) - if userID == "" { - continue - } - - lockKey := lockPrefix + userID - acquired, err := config.RedisClient.SetNX(config.Ctx, lockKey, "locked", lockExpiration).Result() - if err != nil || !acquired { - continue - } - - log.Printf("🔄 Auto-committing cart for user %s (TTL: %v)", userID, ttl) - if err := service.CommitCartToDatabase(userID); err != nil { - log.Printf("❌ Failed to commit cart for %s: %v", userID, err) - } else { - log.Printf("✅ Cart committed for user %s", userID) - } - + val, err := config.RedisClient.Get(ctx, key).Result() + if err != nil { + continue } + + var cart dto.RequestCartDTO + if err := json.Unmarshal([]byte(val), &cart); err != nil { + continue + } + + userID := extractUserIDFromKey(key) + + cartID := SaveCartToDB(ctx, userID, &cart) + + _ = config.RedisClient.Del(ctx, key).Err() + + fmt.Printf( + "[AUTO-COMMIT] UserID: %s | CartID: %s | TotalItem: %d | EstimatedTotalPrice: %.2f | Committed at: %s\n", + userID, cartID, len(cart.CartItems), calculateTotalEstimated(&cart), time.Now().Format(time.RFC3339), + ) + } - if err := iter.Err(); err != nil { - log.Printf("❌ Error iterating Redis keys: %v", err) - } + return nil } func extractUserIDFromKey(key string) string { - if strings.HasPrefix(key, "cart:") { - return strings.TrimPrefix(key, "cart:") + + parts := strings.Split(key, ":") + if len(parts) == 3 { + return parts[2] } return "" } + +func SaveCartToDB(ctx context.Context, userID string, cart *dto.RequestCartDTO) string { + totalAmount := float32(0) + totalPrice := float32(0) + + var cartItems []model.CartItem + for _, item := range cart.CartItems { + + var trash model.TrashCategory + if err := config.DB.First(&trash, "id = ?", item.TrashID).Error; err != nil { + continue + } + + subtotal := trash.EstimatedPrice * float64(item.Amount) + totalAmount += item.Amount + totalPrice += float32(subtotal) + + cartItems = append(cartItems, model.CartItem{ + TrashCategoryID: item.TrashID, + Amount: item.Amount, + SubTotalEstimatedPrice: float32(subtotal), + }) + } + + newCart := model.Cart{ + UserID: userID, + TotalAmount: totalAmount, + EstimatedTotalPrice: totalPrice, + CartItems: cartItems, + } + + if err := config.DB.WithContext(ctx).Create(&newCart).Error; err != nil { + fmt.Printf("Error committing cart: %v\n", err) + } + + return newCart.ID +} + +func calculateTotalEstimated(cart *dto.RequestCartDTO) float32 { + var total float32 + for _, item := range cart.CartItems { + var trash model.TrashCategory + if err := config.DB.First(&trash, "id = ?", item.TrashID).Error; err != nil { + continue + } + total += item.Amount * float32(trash.EstimatedPrice) + } + return total +} diff --git a/model/trashcart_model.go b/model/trashcart_model.go index bfe1467..49d530a 100644 --- a/model/trashcart_model.go +++ b/model/trashcart_model.go @@ -6,13 +6,13 @@ import ( type Cart struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - UserID string `gorm:"not null" json:"userid"` + UserID string `gorm:"not null" json:"user_id"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;" json:"-"` - CartItems []CartItem `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"cartitems"` - TotalAmount float32 `json:"totalamount"` - EstimatedTotalPrice float32 `json:"estimated_totalprice"` - CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` + CartItems []CartItem `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"cart_items"` + TotalAmount float32 `json:"total_amount"` + EstimatedTotalPrice float32 `json:"estimated_total_price"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } type CartItem struct { @@ -22,7 +22,7 @@ type CartItem struct { TrashCategoryID string `gorm:"not null" json:"trash_id"` TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"trash_category"` Amount float32 `json:"amount"` - SubTotalEstimatedPrice float32 `json:"subtotalestimatedprice"` - CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` + SubTotalEstimatedPrice float32 `json:"subtotal_estimated_price"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } diff --git a/presentation/cart_router.go b/presentation/cart_router.go new file mode 100644 index 0000000..30c6725 --- /dev/null +++ b/presentation/cart_router.go @@ -0,0 +1,24 @@ +package presentation + +import ( + "rijig/internal/handler" + "rijig/internal/services" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func TrashCartRouter(api fiber.Router) { + cartService := services.NewCartService() + cartHandler := handler.NewCartHandler(cartService) + + cart := api.Group("/cart") + cart.Use(middleware.AuthMiddleware) + + cart.Get("/", cartHandler.GetCart) + cart.Post("/item", cartHandler.AddOrUpdateCartItem) + cart.Post("/items", cartHandler.AddMultipleCartItems) + cart.Delete("/item/:trashID", cartHandler.DeleteCartItem) + cart.Delete("/", cartHandler.ClearCart) + +} diff --git a/presentation/trashcart_route.go b/presentation/trashcart_route.go deleted file mode 100644 index 277010d..0000000 --- a/presentation/trashcart_route.go +++ /dev/null @@ -1,27 +0,0 @@ -package presentation - -import ( - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/internal/worker" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func TrashCartRouter(api fiber.Router) { - cartRepo := repositories.NewCartRepository() - cartService := services.NewCartService(cartRepo) - cartHandler := handler.NewCartHandler(cartService) - - worker.StartCartCommitWorker(cartService) - - cart := api.Group("/cart", middleware.AuthMiddleware) - cart.Put("/refresh", cartHandler.RefreshCartTTL) - cart.Post("/", cartHandler.AddOrUpdateCartItem) - cart.Get("/", cartHandler.GetCart) - cart.Post("/commit", cartHandler.CommitCart) - cart.Delete("/", cartHandler.ClearCart) - cart.Delete("/:trashid", cartHandler.DeleteCartItem) -}