diff --git a/cmd/cart_worker.go b/cmd/cart_worker.go deleted file mode 100644 index d58aab4..0000000 --- a/cmd/cart_worker.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -// import ( -// "context" -// "log" -// "strings" -// "time" - -// "rijig/config" -// "rijig/internal/services" -// ) - -// // func main() { -// // config.SetupConfig() - -// // } - -// func processCartKeys(ctx context.Context, cartService services.CartService) { -// pattern := "cart:user:*" -// iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() - -// for iter.Next(ctx) { -// key := iter.Val() -// ttl, err := config.RedisClient.TTL(ctx, key).Result() -// if err != nil { -// log.Printf("Failed to get TTL for key %s: %v", key, err) -// continue -// } - -// if ttl <= time.Minute { -// log.Printf("🔄 Auto-committing key: %s", key) -// parts := strings.Split(key, ":") -// if len(parts) != 3 { -// log.Printf("Invalid key format: %s", key) -// continue -// } -// userID := parts[2] - -// err := cartService.CommitCartFromRedis(userID) -// if err != nil { -// log.Printf("❌ Failed to commit cart for user %s: %v", userID, err) -// } else { -// log.Printf("✅ Cart for user %s committed successfully", userID) -// } -// } -// } - -// if err := iter.Err(); err != nil { -// log.Printf("Error iterating keys: %v", err) -// } -// } diff --git a/cmd/main.go b/cmd/main.go index 64aede6..2ab9825 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,14 +1,8 @@ package main import ( - "context" - "log" "rijig/config" - "rijig/internal/repositories" - "rijig/internal/services" "rijig/router" - "strings" - "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -17,67 +11,36 @@ import ( func main() { config.SetupConfig() app := fiber.New() + app.Use(cors.New(cors.Config{ - AllowOrigins: "*", + AllowOrigins: "*", AllowMethods: "GET,POST,PUT,PATCH,DELETE", AllowHeaders: "Content-Type,x-api-key", })) - // Route setup + // app.Use(cors.New(cors.Config{ + // AllowOrigins: "http://localhost:3000", + // AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", + // AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", + // AllowCredentials: true, + // })) + + // app.Use(func(c *fiber.Ctx) error { + // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + // c.Set("Access-Control-Allow-Credentials", "true") + // return c.Next() + // }) + + // app.Options("*", func(c *fiber.Ctx) error { + // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + // c.Set("Access-Control-Allow-Credentials", "true") + // return c.SendStatus(fiber.StatusNoContent) + // }) + router.SetupRoutes(app) - - // Siapkan dependency untuk worker - repoCart := repositories.NewCartRepository() - repoTrash := repositories.NewTrashRepository(config.DB) - cartService := services.NewCartService(repoCart, repoTrash) - ctx := context.Background() - - // ✅ Jalankan worker di background - go func() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - log.Println("🛠️ Cart Worker is running in background...") - for range ticker.C { - processCartKeys(ctx, cartService) - } - }() - - // 🚀 Jalankan server (blocking) config.StartServer(app) -} - -func processCartKeys(ctx context.Context, cartService services.CartService) { - pattern := "cart:user:*" - iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() - - for iter.Next(ctx) { - key := iter.Val() - ttl, err := config.RedisClient.TTL(ctx, key).Result() - if err != nil { - log.Printf("Failed to get TTL for key %s: %v", key, err) - continue - } - - if ttl <= time.Minute { - log.Printf("🔄 Auto-committing key: %s", key) - parts := strings.Split(key, ":") - if len(parts) != 3 { - log.Printf("Invalid key format: %s", key) - continue - } - userID := parts[2] - - err := cartService.CommitCartFromRedis(userID) - if err != nil { - log.Printf("❌ Failed to commit cart for user %s: %v", userID, err) - } else { - log.Printf("✅ Cart for user %s committed successfully", userID) - } - } - } - - if err := iter.Err(); err != nil { - log.Printf("Error iterating keys: %v", err) - } -} +} \ No newline at end of file diff --git a/dto/trash_dto.go b/dto/trash_dto.go index 991ff3d..d1f5fd9 100644 --- a/dto/trash_dto.go +++ b/dto/trash_dto.go @@ -42,10 +42,6 @@ func (r *RequestTrashCategoryDTO) ValidateTrashCategoryInput() (map[string][]str errors["name"] = append(errors["name"], "name is required") } - // if valid, msg := utils.ValidateFloatPrice(fmt.Sprintf("%f", r.EstimatedPrice)); !valid { - // errors["estimated_price"] = append(errors["estimated_price"], msg) - // } - if len(errors) > 0 { return errors, false } @@ -60,10 +56,6 @@ func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string, errors["description"] = append(errors["description"], "description is required") } - // if valid, msg := utils.ValidateFloatPrice(fmt.Sprintf("%f", r.Price)); !valid { - // errors["price"] = append(errors["price"], msg) - // } - if len(errors) > 0 { return errors, false } diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go index f117e49..4c9a0d6 100644 --- a/dto/trashcart_dto.go +++ b/dto/trashcart_dto.go @@ -1,8 +1,8 @@ package dto import ( + "fmt" "strings" - "time" ) type ValidationErrors struct { @@ -19,8 +19,8 @@ type CartResponse struct { CartItems []CartItemResponse `json:"cartitems"` TotalAmount float32 `json:"totalamount"` EstimatedTotalPrice float32 `json:"estimated_totalprice"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } type CartItemResponse struct { @@ -48,3 +48,20 @@ func (r *RequestCartItems) ValidateRequestCartItem() (map[string][]string, bool) 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.TrashID) == "" { + errors[fmt.Sprintf("items[%d].trashid", i)] = append(errors[fmt.Sprintf("items[%d].trashid", i)], "trashid is required") + } + } + if len(errors) > 0 { + return errors, false + } + return nil, true +} diff --git a/go.mod b/go.mod index 4279056..687dde0 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ 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/v3 v3.0.1 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/internal/handler/trashcart_handler.go b/internal/handler/trashcart_handler.go index 5e61d80..7890e77 100644 --- a/internal/handler/trashcart_handler.go +++ b/internal/handler/trashcart_handler.go @@ -9,88 +9,88 @@ import ( ) type CartHandler struct { - CartService services.CartService + Service *services.CartService } -func NewCartHandler(service services.CartService) *CartHandler { - return &CartHandler{ - CartService: service, - } +func NewCartHandler(service *services.CartService) *CartHandler { + return &CartHandler{Service: service} } -// GET /cart - Get cart by user ID -func (h *CartHandler) GetCart(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "unauthorized or invalid user") +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"}, + }) } - cart, err := h.CartService.GetCartByUserID(userID) + 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 { - return utils.InternalServerErrorResponse(c, "failed to retrieve cart") + 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") } - if cart == nil { - return utils.SuccessResponse(c, nil, "Cart is empty") + 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") } -// POST /cart - Create new cart -func (h *CartHandler) CreateCart(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "unauthorized or invalid user") - } - - var reqItems []dto.RequestCartItems - if err := c.BodyParser(&reqItems); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"invalid JSON format"}, - }) - } - - // Logic dipindahkan ke service - if err := h.CartService.CreateCartFromDTO(userID, reqItems); err != nil { - if ve, ok := err.(dto.ValidationErrors); ok { - return utils.ValidationErrorResponse(c, ve.Errors) - } - return utils.InternalServerErrorResponse(c, "failed to create cart") - } - - return utils.CreateResponse(c, nil, "Cart created successfully") -} - - -// DELETE /cart/:id - Delete cart by cartID -func (h *CartHandler) DeleteCart(c *fiber.Ctx) error { - cartID := c.Params("id") - if cartID == "" { - return utils.ErrorResponse(c, "Cart ID is required") - } - - if err := h.CartService.DeleteCart(cartID); err != nil { - return utils.InternalServerErrorResponse(c, "failed to delete cart") - } - - return utils.SuccessResponse(c, nil, "Cart deleted successfully") -} - -// POST /cart/commit - Simpan cart dari Redis ke DB func (h *CartHandler) CommitCart(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "unauthorized or invalid user") - } + userID := c.Locals("userID").(string) - err := h.CartService.CommitCartFromRedis(userID) + err := h.Service.CommitCartToDatabase(userID) if err != nil { - if err.Error() == "cart not found in redis" { - return utils.ErrorResponse(c, "Cart tidak ditemukan atau sudah expired") - } - return utils.InternalServerErrorResponse(c, "Gagal menyimpan cart ke database") + return utils.InternalServerErrorResponse(c, "Failed to commit cart to database") } - return utils.SuccessResponse(c, nil, "Cart berhasil disimpan ke 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/repositories/trash_repo.go b/internal/repositories/trash_repo.go index dd6bab4..99a5e1d 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -15,6 +15,7 @@ type TrashRepository interface { AddDetailToCategory(detail *model.TrashDetail) error GetCategories() ([]model.TrashCategory, error) GetCategoryByID(id string) (*model.TrashCategory, error) + FindCategoryId(id string) (*model.TrashCategory, error) GetTrashCategoryByName(name string) (*model.TrashCategory, error) GetTrashDetailByID(id string) (*model.TrashDetail, error) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) @@ -64,6 +65,15 @@ func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, erro return &category, nil } +func (r *trashRepository) FindCategoryId(id string) (*model.TrashCategory, error) { + var category model.TrashCategory + + if err := r.DB.First(&category, "id = ?", id).Error; err != nil { + return nil, fmt.Errorf("category not found: %v", err) + } + return &category, nil +} + func (r *trashRepository) GetTrashCategoryByName(name string) (*model.TrashCategory, error) { var category model.TrashCategory diff --git a/internal/repositories/trashcart_repo.go b/internal/repositories/trashcart_repo.go index 5ad88b3..59e37ff 100644 --- a/internal/repositories/trashcart_repo.go +++ b/internal/repositories/trashcart_repo.go @@ -1,100 +1,46 @@ package repositories import ( - "errors" - "log" - "rijig/config" "rijig/model" - - "gorm.io/gorm" ) type CartRepository interface { - Create(cart *model.Cart) error - GetByUserID(userID string) (*model.Cart, error) - Update(cart *model.Cart) error - InsertCartItem(item *model.CartItem) error - UpdateCartItem(item *model.CartItem) error - DeleteCartItemByID(id string) error - Delete(cartID string) error - DeleteByUserID(userID string) error + CreateCart(cart *model.Cart) error + GetTrashCategoryByID(id string) (*model.TrashCategory, error) + GetCartByUserID(userID string) (*model.Cart, error) + DeleteCartByUserID(userID string) error } -type cartRepository struct { - db *gorm.DB -} +type cartRepository struct{} func NewCartRepository() CartRepository { - return &cartRepository{ - db: config.DB, - } + return &cartRepository{} } -func (r *cartRepository) Create(cart *model.Cart) error { - tx := r.db.Begin() - if err := tx.Create(cart).Error; err != nil { - tx.Rollback() - return err - } - return tx.Commit().Error +func (r *cartRepository) CreateCart(cart *model.Cart) error { + return config.DB.Create(cart).Error } -func (r *cartRepository) GetByUserID(userID string) (*model.Cart, error) { +func (r *cartRepository) DeleteCartByUserID(userID string) error { + return config.DB.Where("user_id = ?", userID).Delete(&model.Cart{}).Error +} + +func (r *cartRepository) GetTrashCategoryByID(id string) (*model.TrashCategory, error) { + var trash model.TrashCategory + if err := config.DB.First(&trash, "id = ?", id).Error; err != nil { + return nil, err + } + return &trash, nil +} + +func (r *cartRepository) GetCartByUserID(userID string) (*model.Cart, error) { var cart model.Cart - - err := r.db. - Preload("CartItems.TrashCategory"). + err := config.DB.Preload("CartItems.TrashCategory"). Where("user_id = ?", userID). First(&cart).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - log.Printf("Error retrieving cart for user %s: %v", userID, err) - return nil, errors.New("failed to retrieve cart") + return nil, err } - return &cart, nil } - -func (r *cartRepository) Update(cart *model.Cart) error { - err := r.db.Save(cart).Error - if err != nil { - log.Printf("Error updating cart %s: %v", cart.ID, err) - return errors.New("failed to update cart") - } - return nil -} - -func (r *cartRepository) InsertCartItem(item *model.CartItem) error { - return r.db.Create(item).Error -} - -func (r *cartRepository) UpdateCartItem(item *model.CartItem) error { - return r.db.Save(item).Error -} - -func (r *cartRepository) DeleteCartItemByID(id string) error { - return r.db.Delete(&model.CartItem{}, "id = ?", id).Error -} - -func (r *cartRepository) Delete(cartID string) error { - result := r.db.Where("id = ?", cartID).Delete(&model.Cart{}) - if result.Error != nil { - log.Printf("Error deleting cart %s: %v", cartID, result.Error) - return errors.New("failed to delete cart") - } - - if result.RowsAffected == 0 { - log.Printf("Cart with ID %s not found for deletion", cartID) - return errors.New("cart not found") - } - - return nil -} - -func (r *cartRepository) DeleteByUserID(userID string) error { - return r.db.Where("user_id = ?", userID).Delete(&model.Cart{}).Error -} diff --git a/internal/services/trashcart_redisservices.go b/internal/services/trashcart_redisservices.go new file mode 100644 index 0000000..1108fc0 --- /dev/null +++ b/internal/services/trashcart_redisservices.go @@ -0,0 +1,123 @@ +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.TrashID == newItem.TrashID { + 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.TrashID == trashID { + index = i + break + } + } + + if index == -1 { + log.Printf("TrashID %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 1562bb7..939ec40 100644 --- a/internal/services/trashcart_service.go +++ b/internal/services/trashcart_service.go @@ -1,130 +1,135 @@ package services import ( - "encoding/json" - "errors" - "fmt" "log" "time" "rijig/dto" "rijig/internal/repositories" "rijig/model" - "rijig/utils" + + "github.com/google/uuid" ) -type CartService interface { - CreateCartFromDTO(userID string, items []dto.RequestCartItems) error - GetCartByUserID(userID string) (*dto.CartResponse, error) - CommitCartFromRedis(userID string) error - DeleteCart(cartID string) error +type CartService struct { + Repo repositories.CartRepository } -type cartService struct { - repo repositories.CartRepository - repoTrash repositories.TrashRepository +func NewCartService(repo repositories.CartRepository) *CartService { + return &CartService{Repo: repo} } -func NewCartService(repo repositories.CartRepository, repoTrash repositories.TrashRepository) CartService { - return &cartService{repo: repo, repoTrash: repoTrash} -} +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 redisCartKey(userID string) string { - return fmt.Sprintf("cart:user:%s", userID) -} + var cartItems []model.CartItem + var totalAmount float32 + var estimatedTotal float32 -func (s *cartService) CreateCartFromDTO(userID string, items []dto.RequestCartItems) error { - // Validasi semua item for _, item := range items { - if errMap, valid := item.ValidateRequestCartItem(); !valid { - return dto.ValidationErrors{Errors: errMap} - } - } - - // Ambil cart yang sudah ada dari Redis (jika ada) - var existingCart dto.CartResponse - val, err := utils.GetData(redisCartKey(userID)) - if err == nil && val != "" { - if err := json.Unmarshal([]byte(val), &existingCart); err != nil { - log.Printf("Failed to unmarshal existing cart: %v", err) - } - } - - // Buat map dari existing items untuk mempermudah update - itemMap := make(map[string]dto.CartItemResponse) - for _, item := range existingCart.CartItems { - itemMap[item.TrashName] = item - } - - // Proses input baru - for _, input := range items { - trash, err := s.repoTrash.GetCategoryByID(input.TrashID) + trash, err := s.Repo.GetTrashCategoryByID(item.TrashID) if err != nil { - return fmt.Errorf("failed to retrieve trash category for id %s: %v", input.TrashID, err) - } - - if input.Amount == 0 { - delete(itemMap, trash.Name) // hapus item + log.Printf("Trash category not found for trashID: %s", item.TrashID) continue } - subtotal := float32(trash.EstimatedPrice) * input.Amount - - itemMap[trash.Name] = dto.CartItemResponse{ - TrashIcon: trash.Icon, - TrashName: trash.Name, - Amount: input.Amount, - EstimatedSubTotalPrice: subtotal, - } - } - - // Rekonstruksi cart - var finalItems []dto.CartItemResponse - var totalAmount float32 - var totalPrice float32 - for _, item := range itemMap { - finalItems = append(finalItems, item) + subTotal := float32(trash.EstimatedPrice) * item.Amount totalAmount += item.Amount - totalPrice += item.EstimatedSubTotalPrice + estimatedTotal += subTotal + + cartItems = append(cartItems, model.CartItem{ + ID: uuid.NewString(), + TrashID: item.TrashID, + Amount: item.Amount, + SubTotalEstimatedPrice: subTotal, + }) } - cart := dto.CartResponse{ - ID: existingCart.ID, + cart := &model.Cart{ + ID: uuid.NewString(), UserID: userID, + CartItems: cartItems, TotalAmount: totalAmount, - EstimatedTotalPrice: totalPrice, - CartItems: finalItems, + EstimatedTotalPrice: estimatedTotal, CreatedAt: time.Now(), UpdatedAt: time.Now(), } - // Simpan ulang ke Redis dengan TTL 10 menit - return utils.SetData(redisCartKey(userID), cart, 1*time.Minute) + 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 := ClearCart(userID); err != nil { + log.Printf("Failed to clear Redis cart: %v", err) + } + + log.Printf("Cart committed successfully for user: %s", userID) + return nil } - -func (s *cartService) GetCartByUserID(userID string) (*dto.CartResponse, error) { - val, err := utils.GetData(redisCartKey(userID)) - if err != nil { - log.Printf("Redis get error: %v", err) +func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) { + items, err := GetCartItems(userID) + if err != nil || len(items) == 0 { + return nil, err } - if val != "" { - var cached dto.CartResponse - if err := json.Unmarshal([]byte(val), &cached); err == nil { - return &cached, nil + + var totalAmount float32 + var estimatedTotal float32 + var cartItemDTOs []dto.CartItemResponse + + for _, item := range items { + trash, err := s.Repo.GetTrashCategoryByID(item.TrashID) + if err != nil { + continue } + + subtotal := float32(trash.EstimatedPrice) * item.Amount + totalAmount += item.Amount + estimatedTotal += subtotal + + cartItemDTOs = append(cartItemDTOs, dto.CartItemResponse{ + TrashIcon: trash.Icon, + TrashName: trash.Name, + Amount: item.Amount, + EstimatedSubTotalPrice: subtotal, + }) } - cart, err := s.repo.GetByUserID(userID) + 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) { + + 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 } - if cart == nil { - return nil, nil - } var items []dto.CartItemResponse - for _, item := range cart.CartItems { + for _, item := range cartDB.CartItems { items = append(items, dto.CartItemResponse{ TrashIcon: item.TrashCategory.Icon, TrashName: item.TrashCategory.Name, @@ -133,108 +138,14 @@ func (s *cartService) GetCartByUserID(userID string) (*dto.CartResponse, error) }) } - response := &dto.CartResponse{ - ID: cart.ID, - UserID: cart.UserID, - TotalAmount: cart.TotalAmount, - EstimatedTotalPrice: cart.EstimatedTotalPrice, + 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, - CreatedAt: cart.CreatedAt, - UpdatedAt: cart.UpdatedAt, } - - return response, nil -} - -func (s *cartService) CommitCartFromRedis(userID string) error { - val, err := utils.GetData(redisCartKey(userID)) - if err != nil || val == "" { - return errors.New("no cart found in redis") - } - - var cartDTO dto.CartResponse - if err := json.Unmarshal([]byte(val), &cartDTO); err != nil { - return errors.New("invalid cart data in Redis") - } - - existingCart, err := s.repo.GetByUserID(userID) - if err != nil { - return fmt.Errorf("failed to get cart from db: %v", err) - } - - if existingCart == nil { - // buat cart baru jika belum ada - var items []model.CartItem - for _, item := range cartDTO.CartItems { - trash, err := s.repoTrash.GetTrashCategoryByName(item.TrashName) - if err != nil { - continue - } - - items = append(items, model.CartItem{ - TrashID: trash.ID, - Amount: item.Amount, - SubTotalEstimatedPrice: item.EstimatedSubTotalPrice, - }) - } - - newCart := model.Cart{ - UserID: userID, - TotalAmount: cartDTO.TotalAmount, - EstimatedTotalPrice: cartDTO.EstimatedTotalPrice, - CartItems: items, - } - - return s.repo.Create(&newCart) - } - - // buat map item lama (by trash_name) - existingItemMap := make(map[string]*model.CartItem) - for i := range existingCart.CartItems { - trashName := existingCart.CartItems[i].TrashCategory.Name - existingItemMap[trashName] = &existingCart.CartItems[i] - } - - // proses update/hapus/tambah - for _, newItem := range cartDTO.CartItems { - if newItem.Amount == 0 { - if existing, ok := existingItemMap[newItem.TrashName]; ok { - _ = s.repo.DeleteCartItemByID(existing.ID) - } - continue - } - - trash, err := s.repoTrash.GetTrashCategoryByName(newItem.TrashName) - if err != nil { - continue - } - - if existing, ok := existingItemMap[newItem.TrashName]; ok { - existing.Amount = newItem.Amount - existing.SubTotalEstimatedPrice = newItem.EstimatedSubTotalPrice - _ = s.repo.UpdateCartItem(existing) - } else { - newModelItem := model.CartItem{ - CartID: existingCart.ID, - TrashID: trash.ID, - Amount: newItem.Amount, - SubTotalEstimatedPrice: newItem.EstimatedSubTotalPrice, - } - _ = s.repo.InsertCartItem(&newModelItem) - } - } - - // update cart total amount & price - existingCart.TotalAmount = cartDTO.TotalAmount - existingCart.EstimatedTotalPrice = cartDTO.EstimatedTotalPrice - if err := s.repo.Update(existingCart); err != nil { - return err - } - - return utils.DeleteData(redisCartKey(userID)) -} - - -func (s *cartService) DeleteCart(cartID string) error { - return s.repo.Delete(cartID) + return resp, nil } diff --git a/internal/worker/cart_committer.go b/internal/worker/cart_committer.go deleted file mode 100644 index efe45aa..0000000 --- a/internal/worker/cart_committer.go +++ /dev/null @@ -1,58 +0,0 @@ -package worker - -import ( - "context" - "encoding/json" - "log" - - "rijig/config" - "rijig/internal/repositories" - "rijig/model" -) - -type CartCommitter struct { - repo repositories.CartRepository -} - -func NewCartCommitter(repo repositories.CartRepository) *CartCommitter { - return &CartCommitter{repo: repo} -} - -func (cc *CartCommitter) RunAutoCommit() { - ctx := context.Background() - pattern := "cart:user:*" - - iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() - for iter.Next(ctx) { - key := iter.Val() - - val, err := config.RedisClient.Get(ctx, key).Result() - if err != nil { - log.Printf("Error fetching key %s: %v", key, err) - continue - } - - var cart model.Cart - if err := json.Unmarshal([]byte(val), &cart); err != nil { - log.Printf("Invalid cart format in key %s: %v", key, err) - continue - } - - // Simpan ke DB - if err := cc.repo.Create(&cart); err != nil { - log.Printf("Failed to commit cart to DB from key %s: %v", key, err) - continue - } - - // Delete from Redis - if err := config.RedisClient.Del(ctx, key).Err(); err != nil { - log.Printf("Failed to delete key %s after commit: %v", key, err) - } else { - log.Printf("Committed and deleted key %s successfully", key) - } - } - - if err := iter.Err(); err != nil { - log.Printf("Redis scan error: %v", err) - } -} diff --git a/internal/worker/cart_worker.go b/internal/worker/cart_worker.go new file mode 100644 index 0000000..ce208ab --- /dev/null +++ b/internal/worker/cart_worker.go @@ -0,0 +1,76 @@ +package worker + +import ( + "log" + "strings" + "time" + + "rijig/config" + "rijig/internal/services" +) + +const ( + lockPrefix = "lock:cart:" + lockExpiration = 30 * time.Second + commitThreshold = 20 * time.Second + scanPattern = "cart:*" +) + +func StartCartCommitWorker(service *services.CartService) { + + 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) + 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) + } + + } + } + + if err := iter.Err(); err != nil { + log.Printf("❌ Error iterating Redis keys: %v", err) + } +} + +func extractUserIDFromKey(key string) string { + if strings.HasPrefix(key, "cart:") { + return strings.TrimPrefix(key, "cart:") + } + return "" +} diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index 400798b..9549e25 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -35,6 +35,10 @@ func AuthMiddleware(c *fiber.Ctx) error { return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims") } + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Unexpected signing method") + } + userID := claims["sub"].(string) deviceID := claims["device_id"].(string) diff --git a/presentation/trashcart_route.go b/presentation/trashcart_route.go index a7313b7..277010d 100644 --- a/presentation/trashcart_route.go +++ b/presentation/trashcart_route.go @@ -1,26 +1,27 @@ package presentation import ( - "rijig/config" "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() - trashRepo := repositories.NewTrashRepository(config.DB) - cartService := services.NewCartService(cartRepo, trashRepo) + cartService := services.NewCartService(cartRepo) cartHandler := handler.NewCartHandler(cartService) - cart := api.Group("/cart") - cart.Use(middleware.AuthMiddleware) - cart.Post("/", cartHandler.CreateCart) + 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("/:id", cartHandler.DeleteCart) + cart.Delete("/", cartHandler.ClearCart) + cart.Delete("/:trashid", cartHandler.DeleteCartItem) }