From 0467598e58de6663f8654549ecb6ffffec672be8 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 19 May 2025 02:16:24 +0700 Subject: [PATCH] feat: add feature cart worker and auto commit redis to Db --- cmd/cart_worker.go | 51 +++++ cmd/main.go | 86 ++++++--- config/database.go | 3 + dto/trashcart_dto.go | 50 +++++ go.mod | 1 + go.sum | 2 + internal/handler/trashcart_handler.go | 96 ++++++++++ internal/repositories/trash_repo.go | 10 + internal/repositories/trashcart_repo.go | 100 ++++++++++ internal/services/trashcart_service.go | 240 ++++++++++++++++++++++++ internal/worker/cart_committer.go | 58 ++++++ model/trashcart_model.go | 34 ++-- presentation/trashcart_route.go | 26 +++ router/setup_routes.go.go | 1 + 14 files changed, 719 insertions(+), 39 deletions(-) create mode 100644 cmd/cart_worker.go create mode 100644 dto/trashcart_dto.go create mode 100644 internal/handler/trashcart_handler.go create mode 100644 internal/repositories/trashcart_repo.go create mode 100644 internal/services/trashcart_service.go create mode 100644 internal/worker/cart_committer.go create mode 100644 presentation/trashcart_route.go diff --git a/cmd/cart_worker.go b/cmd/cart_worker.go new file mode 100644 index 0000000..d58aab4 --- /dev/null +++ b/cmd/cart_worker.go @@ -0,0 +1,51 @@ +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 024c357..64aede6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,8 +1,14 @@ 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" @@ -11,37 +17,67 @@ 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", })) - // 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) - // }) - + // Route setup 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) + } +} diff --git a/config/database.go b/config/database.go index e9e920c..95c9859 100644 --- a/config/database.go +++ b/config/database.go @@ -53,6 +53,9 @@ func ConnectDatabase() { // =>requestpickup preparation<= &model.RequestPickup{}, &model.RequestPickupItem{}, + + &model.Cart{}, + &model.CartItem{}, // =>requestpickup preparation<= // =>store preparation<= diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go new file mode 100644 index 0000000..f117e49 --- /dev/null +++ b/dto/trashcart_dto.go @@ -0,0 +1,50 @@ +package dto + +import ( + "strings" + "time" +) + +type ValidationErrors struct { + Errors map[string][]string +} + +func (v ValidationErrors) Error() string { + return "validation error" +} + +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 time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type CartItemResponse struct { + TrashIcon string `json:"trashicon"` + TrashName string `json:"trashname"` + Amount float32 `json:"amount"` + EstimatedSubTotalPrice float32 `json:"estimated_subtotalprice"` +} + +type RequestCartItems struct { + TrashID string `json:"trashid"` + Amount float32 `json:"amount"` +} + +func (r *RequestCartItems) ValidateRequestCartItem() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.TrashID) == "" { + errors["trashid"] = append(errors["trashid"], "trashid is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/go.mod b/go.mod index 687dde0..4279056 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/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/go.sum b/go.sum index 83b4326..d5b6c0b 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/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= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= diff --git a/internal/handler/trashcart_handler.go b/internal/handler/trashcart_handler.go new file mode 100644 index 0000000..5e61d80 --- /dev/null +++ b/internal/handler/trashcart_handler.go @@ -0,0 +1,96 @@ +package handler + +import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CartHandler struct { + CartService services.CartService +} + +func NewCartHandler(service services.CartService) *CartHandler { + return &CartHandler{ + CartService: 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") + } + + cart, err := h.CartService.GetCartByUserID(userID) + if err != nil { + return utils.InternalServerErrorResponse(c, "failed to retrieve cart") + } + + if cart == nil { + return utils.SuccessResponse(c, nil, "Cart is empty") + } + + 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") + } + + err := h.CartService.CommitCartFromRedis(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.SuccessResponse(c, nil, "Cart berhasil disimpan ke database") +} diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index 380716a..dd6bab4 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) + GetTrashCategoryByName(name string) (*model.TrashCategory, error) GetTrashDetailByID(id string) (*model.TrashDetail, error) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) UpdateCategoryName(id string, newName string) error @@ -63,6 +64,15 @@ func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, erro return &category, nil } +func (r *trashRepository) GetTrashCategoryByName(name string) (*model.TrashCategory, error) { + var category model.TrashCategory + + if err := r.DB.Find(&category, "name = ?", name).Error; err != nil { + return nil, fmt.Errorf("category not found: %v", err) + } + return &category, nil +} + func (r *trashRepository) GetTrashDetailByID(id string) (*model.TrashDetail, error) { var detail model.TrashDetail if err := r.DB.First(&detail, "id = ?", id).Error; err != nil { diff --git a/internal/repositories/trashcart_repo.go b/internal/repositories/trashcart_repo.go new file mode 100644 index 0000000..5ad88b3 --- /dev/null +++ b/internal/repositories/trashcart_repo.go @@ -0,0 +1,100 @@ +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 +} + +type cartRepository struct { + db *gorm.DB +} + +func NewCartRepository() CartRepository { + return &cartRepository{ + db: config.DB, + } +} + +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) GetByUserID(userID string) (*model.Cart, error) { + var cart model.Cart + + err := r.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 &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_service.go b/internal/services/trashcart_service.go new file mode 100644 index 0000000..1562bb7 --- /dev/null +++ b/internal/services/trashcart_service.go @@ -0,0 +1,240 @@ +package services + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "time" + + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" +) + +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 + repoTrash repositories.TrashRepository +} + +func NewCartService(repo repositories.CartRepository, repoTrash repositories.TrashRepository) CartService { + return &cartService{repo: repo, repoTrash: repoTrash} +} + +func redisCartKey(userID string) string { + return fmt.Sprintf("cart:user:%s", userID) +} + +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) + 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 + 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) + totalAmount += item.Amount + totalPrice += item.EstimatedSubTotalPrice + } + + cart := dto.CartResponse{ + ID: existingCart.ID, + UserID: userID, + TotalAmount: totalAmount, + EstimatedTotalPrice: totalPrice, + CartItems: finalItems, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // Simpan ulang ke Redis dengan TTL 10 menit + return utils.SetData(redisCartKey(userID), cart, 1*time.Minute) +} + + +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) + } + if val != "" { + var cached dto.CartResponse + if err := json.Unmarshal([]byte(val), &cached); err == nil { + return &cached, nil + } + } + + cart, err := s.repo.GetByUserID(userID) + if err != nil { + return nil, err + } + if cart == nil { + return nil, nil + } + + var items []dto.CartItemResponse + for _, item := range cart.CartItems { + items = append(items, dto.CartItemResponse{ + TrashIcon: item.TrashCategory.Icon, + TrashName: item.TrashCategory.Name, + Amount: item.Amount, + EstimatedSubTotalPrice: item.SubTotalEstimatedPrice, + }) + } + + response := &dto.CartResponse{ + ID: cart.ID, + UserID: cart.UserID, + TotalAmount: cart.TotalAmount, + EstimatedTotalPrice: cart.EstimatedTotalPrice, + 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) +} diff --git a/internal/worker/cart_committer.go b/internal/worker/cart_committer.go new file mode 100644 index 0000000..efe45aa --- /dev/null +++ b/internal/worker/cart_committer.go @@ -0,0 +1,58 @@ +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/model/trashcart_model.go b/model/trashcart_model.go index af23000..97aa8d0 100644 --- a/model/trashcart_model.go +++ b/model/trashcart_model.go @@ -1,21 +1,27 @@ package model -import "time" +import ( + "time" +) type Cart struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - UserId string `gorm:"not null" json:"userid"` - User User `gorm:"foreignKey:UserId;constraint:OnDelete:CASCADE;" json:"user"` - CartItem []CartItems `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"cartitems"` - TotalAmount float32 `json:"totalamount"` - EstimatedTotalPrice float32 `json:"estimated_totalprice"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + UserID string `gorm:"not null" json:"userid"` + 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:"autoCreateTime" json:"createdAt"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` } -type CartItems struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - TrashId string `json:"trashid"` - TrashCategory TrashCategory `gorm:"foreignKey:TrashId;constraint:OnDelete:CASCADE;" json:"trash"` - Amount float32 `json:"amount"` - CreaatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +type CartItem struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + CartID string `gorm:"not null" json:"-"` + TrashID string `gorm:"not null" json:"trashid"` + TrashCategory TrashCategory `gorm:"foreignKey:TrashID;constraint:OnDelete:CASCADE;" json:"trash"` + Amount float32 `json:"amount"` + SubTotalEstimatedPrice float32 `json:"subtotalestimatedprice"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` } diff --git a/presentation/trashcart_route.go b/presentation/trashcart_route.go new file mode 100644 index 0000000..a7313b7 --- /dev/null +++ b/presentation/trashcart_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "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) + cartHandler := handler.NewCartHandler(cartService) + + cart := api.Group("/cart") + cart.Use(middleware.AuthMiddleware) + cart.Post("/", cartHandler.CreateCart) + cart.Get("/", cartHandler.GetCart) + cart.Post("/commit", cartHandler.CommitCart) + cart.Delete("/:id", cartHandler.DeleteCart) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index ecfad81..d23059d 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -28,6 +28,7 @@ func SetupRoutes(app *fiber.App) { presentation.CompanyProfileRouter(api) presentation.RequestPickupRouter(api) presentation.CollectorRouter(api) + presentation.TrashCartRouter(api) presentation.UserProfileRouter(api) presentation.UserPinRouter(api)