fix: fixing cart feature behavior and auto commit optimizing

This commit is contained in:
pahmiudahgede 2025-05-22 01:42:14 +07:00
parent 226d188ece
commit b7a1d10898
14 changed files with 550 additions and 480 deletions

View File

@ -1,15 +1,29 @@
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{

View File

@ -5,43 +5,48 @@ 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 {
type ResponseCartItemDTO 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"`
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")
}
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 {
@ -50,20 +55,3 @@ 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.TrashCategoryID) == "" {
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
}

1
go.mod
View File

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

2
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
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()
keys, err := config.RedisClient.Keys(ctx, "cart:user:*").Result()
if err != nil {
log.Printf("❌ Error getting TTL for %s: %v", key, err)
return fmt.Errorf("error fetching cart keys: %w", err)
}
for _, key := range keys {
ttl, err := config.RedisClient.TTL(ctx, key).Result()
if err != nil || ttl > 30*time.Second {
continue
}
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
}
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
}
cartID := SaveCartToDB(ctx, userID, &cart)
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)
}
_ = 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
}

View File

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

View File

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

View File

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