fix: auto commit to DB

This commit is contained in:
pahmiudahgede 2025-05-29 16:44:47 +07:00
parent 29b04db0cb
commit 58f843cac2
17 changed files with 627 additions and 286 deletions

View File

@ -2,58 +2,49 @@ package main
import (
"log"
"os"
"rijig/config"
"rijig/internal/repositories"
"rijig/internal/services"
"rijig/internal/worker"
"rijig/router"
"time"
"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)
cartRepo := repositories.NewCartRepository()
trashRepo := repositories.NewTrashRepository(config.DB)
cartService := services.NewCartService(cartRepo, trashRepo)
worker := worker.NewCartWorker(cartService, cartRepo, trashRepo)
go func() {
c := cron.New()
c.AddFunc("@every 1m", func() {
_ = worker.CommitExpiredCartsToDB()
})
c.Start()
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
if err := worker.AutoCommitExpiringCarts(); err != nil {
log.Printf("Auto-commit error: %v", err)
}
}
}()
app := fiber.New()
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowMethods: "GET,POST,PUT,PATCH,DELETE",
AllowHeaders: "Content-Type,x-api-key",
}))
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{
"error": err.Error(),
})
},
})
// 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(cors.New())
// 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)
config.StartServer(app)

View File

@ -7,44 +7,38 @@ import (
type RequestCartItemDTO struct {
TrashID string `json:"trash_id"`
Amount float32 `json:"amount"`
Amount float64 `json:"amount"`
}
type RequestCartDTO struct {
CartItems []RequestCartItemDTO `json:"cart_items"`
}
type ResponseCartDTO struct {
ID string `json:"id"`
UserID string `json:"user_id"`
TotalAmount float64 `json:"total_amount"`
EstimatedTotalPrice float64 `json:"estimated_total_price"`
CartItems []ResponseCartItemDTO `json:"cart_items"`
}
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"`
SubTotalEstimatedPrice float32 `json:"subtotal_estimated_price"`
}
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"`
TrashPrice float64 `json:"trash_price"`
Amount float64 `json:"amount"`
SubTotalEstimatedPrice float64 `json:"subtotal_estimated_price"`
}
func (r *RequestCartDTO) ValidateRequestCartDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
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 {

1
go.mod
View File

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

View File

@ -1,7 +1,6 @@
package handler
import (
"context"
"rijig/dto"
"rijig/internal/services"
"rijig/utils"
@ -9,100 +8,86 @@ import (
"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 {
cartService services.CartService
}
type cartHandler struct {
service services.CartService
func NewCartHandler(cartService services.CartService) *CartHandler {
return &CartHandler{cartService: cartService}
}
func NewCartHandler(service services.CartService) CartHandler {
return &cartHandler{service: service}
}
func (h *cartHandler) GetCart(c *fiber.Ctx) error {
func (h *CartHandler) AddOrUpdateItem(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
var req dto.RequestCartItemDTO
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")
}
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 {
if err := c.BodyParser(&req); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{
"trash_id": {"harus diisi"},
"amount": {"harus lebih dari 0"},
"request": {"Payload tidak valid"},
})
}
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")
}
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 {
hasErrors, _ := req.Amount > 0 && req.TrashID != "", true
if !hasErrors {
errs := make(map[string][]string)
if req.Amount <= 0 {
errs["amount"] = append(errs["amount"], "Amount harus lebih dari 0")
}
if req.TrashID == "" {
errs["trash_id"] = append(errs["trash_id"], "Trash ID tidak boleh kosong")
}
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())
}
if err := h.cartService.AddOrUpdateItem(c.Context(), userID, req); err != nil {
return utils.InternalServerErrorResponse(c, "Gagal menambahkan item ke keranjang")
}
return utils.SuccessResponse(c, nil, "Semua item berhasil ditambahkan/diupdate ke cart")
return utils.GenericResponse(c, fiber.StatusOK, "Item berhasil ditambahkan ke keranjang")
}
func (h *cartHandler) DeleteCartItem(c *fiber.Ctx) error {
func (h *CartHandler) GetCart(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
trashID := c.Params("trashID")
cart, err := h.cartService.GetCart(c.Context(), userID)
if err != nil {
return utils.ErrorResponse(c, "Gagal mengambil data keranjang")
}
return utils.SuccessResponse(c, cart, "Berhasil mengambil data keranjang")
}
func (h *CartHandler) DeleteItem(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
trashID := c.Params("trash_id")
if trashID == "" {
return utils.ValidationErrorResponse(c, map[string][]string{"trash_id": {"tidak boleh kosong"}})
return utils.GenericResponse(c, fiber.StatusBadRequest, "Trash ID tidak boleh kosong")
}
err := h.service.DeleteItem(context.Background(), userID, trashID)
if err != nil {
return utils.InternalServerErrorResponse(c, err.Error())
if err := h.cartService.DeleteItem(c.Context(), userID, trashID); err != nil {
return utils.InternalServerErrorResponse(c, "Gagal menghapus item dari keranjang")
}
return utils.SuccessResponse(c, nil, "Item berhasil dihapus dari cart")
return utils.GenericResponse(c, fiber.StatusOK, "Item berhasil dihapus dari keranjang")
}
func (h *cartHandler) ClearCart(c *fiber.Ctx) error {
func (h *CartHandler) Checkout(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())
if err := h.cartService.Checkout(c.Context(), userID); err != nil {
return utils.InternalServerErrorResponse(c, "Gagal melakukan checkout keranjang")
}
return utils.SuccessResponse(c, nil, "Seluruh cart berhasil dihapus")
return utils.GenericResponse(c, fiber.StatusOK, "Checkout berhasil. Permintaan pickup telah dibuat.")
}
func (h *CartHandler) ClearCart(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
err := h.cartService.ClearCart(c.Context(), userID)
if err != nil {
return utils.InternalServerErrorResponse(c, "Gagal menghapus keranjang")
}
return utils.GenericResponse(c, fiber.StatusOK, "Keranjang berhasil dikosongkan")
}

View File

@ -1,15 +1,27 @@
package repositories
import (
"context"
"errors"
"fmt"
"rijig/config"
"rijig/model"
"gorm.io/gorm"
)
type CartRepository interface {
CreateCart(cart *model.Cart) error
GetTrashCategoryByID(id string) (*model.TrashCategory, error)
GetCartByUserID(userID string) (*model.Cart, error)
DeleteCartByUserID(userID string) error
FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error)
AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error
DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error
GetCartByUser(ctx context.Context, userID string) (*model.Cart, error)
UpdateCartTotals(ctx context.Context, cartID string) error
DeleteCart(ctx context.Context, userID string) error
// New method for batch cart creation
CreateCartWithItems(ctx context.Context, cart *model.Cart) error
// Check if user already has a cart
HasExistingCart(ctx context.Context, userID string) (bool, error)
}
type cartRepository struct{}
@ -18,29 +30,137 @@ func NewCartRepository() CartRepository {
return &cartRepository{}
}
func (r *cartRepository) CreateCart(cart *model.Cart) error {
return config.DB.Create(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) {
func (r *cartRepository) FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error) {
var cart model.Cart
err := config.DB.Preload("CartItems.TrashCategory").
db := config.DB.WithContext(ctx)
err := db.
Preload("CartItems.TrashCategory").
Where("user_id = ?", userID).
First(&cart).Error
if err == nil {
return &cart, nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
newCart := model.Cart{
UserID: userID,
TotalAmount: 0,
EstimatedTotalPrice: 0,
}
if err := db.Create(&newCart).Error; err != nil {
return nil, err
}
return &newCart, nil
}
return nil, err
}
func (r *cartRepository) AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error {
db := config.DB.WithContext(ctx)
var item model.CartItem
err := db.
Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID).
First(&item).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
newItem := model.CartItem{
CartID: cartID,
TrashCategoryID: trashCategoryID,
Amount: amount,
SubTotalEstimatedPrice: amount * estimatedPrice,
}
return db.Create(&newItem).Error
}
if err != nil {
return err
}
item.Amount = amount
item.SubTotalEstimatedPrice = amount * estimatedPrice
return db.Save(&item).Error
}
func (r *cartRepository) DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error {
db := config.DB.WithContext(ctx)
return db.Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID).
Delete(&model.CartItem{}).Error
}
func (r *cartRepository) GetCartByUser(ctx context.Context, userID string) (*model.Cart, error) {
var cart model.Cart
db := config.DB.WithContext(ctx)
err := db.
Preload("CartItems.TrashCategory").
Where("user_id = ?", userID).
First(&cart).Error
if err != nil {
return nil, err
}
return &cart, nil
}
func (r *cartRepository) UpdateCartTotals(ctx context.Context, cartID string) error {
db := config.DB.WithContext(ctx)
var items []model.CartItem
if err := db.Where("cart_id = ?", cartID).Find(&items).Error; err != nil {
return err
}
var totalAmount float64
var totalPrice float64
for _, item := range items {
totalAmount += item.Amount
totalPrice += item.SubTotalEstimatedPrice
}
return db.Model(&model.Cart{}).
Where("id = ?", cartID).
Updates(map[string]interface{}{
"total_amount": totalAmount,
"estimated_total_price": totalPrice,
}).Error
}
func (r *cartRepository) DeleteCart(ctx context.Context, userID string) error {
db := config.DB.WithContext(ctx)
var cart model.Cart
if err := db.Where("user_id = ?", userID).First(&cart).Error; err != nil {
return err
}
return db.Delete(&cart).Error
}
// New method for batch cart creation with transaction
func (r *cartRepository) CreateCartWithItems(ctx context.Context, cart *model.Cart) error {
db := config.DB.WithContext(ctx)
// Use transaction to ensure data consistency
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(cart).Error; err != nil {
return fmt.Errorf("failed to create cart: %w", err)
}
return nil
})
}
// Check if user already has a cart
func (r *cartRepository) HasExistingCart(ctx context.Context, userID string) (bool, error) {
db := config.DB.WithContext(ctx)
var count int64
err := db.Model(&model.Cart{}).Where("user_id = ?", userID).Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}

View File

@ -31,7 +31,7 @@ func NewAuthMasyarakatService(userRepo repositories.UserRepository, roleRepo rep
func (s *authMasyarakatService) generateJWTToken(userID string, deviceID string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
expirationTime := time.Now().Add(672 * time.Hour)
claims := jwt.MapClaims{
"sub": userID,

View File

@ -31,7 +31,7 @@ func NewAuthPengelolaService(userRepo repositories.UserRepository, roleRepo repo
func (s *authPengelolaService) generateJWTToken(userID string, deviceID string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
expirationTime := time.Now().Add(168 * time.Hour)
claims := jwt.MapClaims{
"sub": userID,

View File

@ -31,7 +31,7 @@ func NewAuthPengepulService(userRepo repositories.UserRepository, roleRepo repos
func (s *authPengepulService) generateJWTToken(userID string, deviceID string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
expirationTime := time.Now().Add(480 * time.Hour)
claims := jwt.MapClaims{
"sub": userID,

View File

@ -8,97 +8,66 @@ import (
"rijig/config"
"rijig/dto"
"github.com/go-redis/redis/v8"
)
var cartTTL = 30 * time.Minute
const CartTTL = 30 * time.Minute
const CartKeyPrefix = "cart:"
func getCartKey(userID string) string {
return fmt.Sprintf("cart:user:%s", userID)
func buildCartKey(userID string) string {
return fmt.Sprintf("%s%s", CartKeyPrefix, 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)
return 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 config.RedisClient.Set(ctx, buildCartKey(userID), data, CartTTL).Err()
}
return nil
func RefreshCartTTL(ctx context.Context, userID string) error {
return config.RedisClient.Expire(ctx, buildCartKey(userID), CartTTL).Err()
}
func GetCartFromRedis(ctx context.Context, userID string) (*dto.RequestCartDTO, error) {
key := getCartKey(userID)
val, err := config.RedisClient.Get(ctx, key).Result()
if err != nil {
val, err := config.RedisClient.Get(ctx, buildCartKey(userID)).Result()
if err == redis.Nil {
return nil, nil
} else 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 nil, err
}
return &cart, nil
}
func DeleteCartFromRedis(ctx context.Context, userID string) error {
key := getCartKey(userID)
return config.RedisClient.Del(ctx, key).Err()
return config.RedisClient.Del(ctx, buildCartKey(userID)).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)
func GetExpiringCartKeys(ctx context.Context, threshold time.Duration) ([]string, error) {
keys, err := config.RedisClient.Keys(ctx, CartKeyPrefix+"*").Result()
if err != nil {
cart = &dto.RequestCartDTO{
CartItems: []dto.RequestCartItemDTO{item},
}
return SetCartToRedis(ctx, userID, *cart)
return nil, err
}
updated := false
for i, ci := range cart.CartItems {
if ci.TrashID == item.TrashID {
cart.CartItems[i].Amount = item.Amount
updated = true
break
var expiringKeys []string
for _, key := range keys {
ttl, err := config.RedisClient.TTL(ctx, key).Result()
if err != nil {
continue
}
if ttl > 0 && ttl <= threshold {
expiringKeys = append(expiringKeys, key)
}
}
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)
return expiringKeys, nil
}

View File

@ -1,35 +1,266 @@
package services
import (
"rijig/dto"
"context"
"errors"
"log"
"rijig/dto"
"rijig/internal/repositories"
"rijig/model"
)
type CartService interface {
GetCart(ctx context.Context, userID string) (*dto.RequestCartDTO, error)
AddOrUpdateItem(ctx context.Context, userID string, item dto.RequestCartItemDTO) error
AddOrUpdateItem(ctx context.Context, userID string, req dto.RequestCartItemDTO) error
GetCart(ctx context.Context, userID string) (*dto.ResponseCartDTO, error)
DeleteItem(ctx context.Context, userID string, trashID string) error
ClearCart(ctx context.Context, userID string) error
Checkout(ctx context.Context, userID string) error
}
type cartService struct{}
func NewCartService() CartService {
return &cartService{}
type cartService struct {
repo repositories.CartRepository
trashRepo repositories.TrashRepository
}
func (s *cartService) GetCart(ctx context.Context, userID string) (*dto.RequestCartDTO, error) {
return GetCartFromRedis(ctx, userID)
func NewCartService(repo repositories.CartRepository, trashRepo repositories.TrashRepository) CartService {
return &cartService{repo: repo, trashRepo: trashRepo}
}
func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, item dto.RequestCartItemDTO) error {
return UpdateOrAddCartItemToRedis(ctx, userID, item)
func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, req dto.RequestCartItemDTO) error {
if req.Amount <= 0 {
return errors.New("amount harus lebih dari 0")
}
_, err := s.trashRepo.GetTrashCategoryByID(ctx, req.TrashID)
if err != nil {
return err
}
existingCart, err := GetCartFromRedis(ctx, userID)
if err != nil {
return err
}
if existingCart == nil {
existingCart = &dto.RequestCartDTO{
CartItems: []dto.RequestCartItemDTO{},
}
}
updated := false
for i, item := range existingCart.CartItems {
if item.TrashID == req.TrashID {
existingCart.CartItems[i].Amount = req.Amount
updated = true
break
}
}
if !updated {
existingCart.CartItems = append(existingCart.CartItems, dto.RequestCartItemDTO{
TrashID: req.TrashID,
Amount: req.Amount,
})
}
return SetCartToRedis(ctx, userID, *existingCart)
}
func (s *cartService) GetCart(ctx context.Context, userID string) (*dto.ResponseCartDTO, error) {
cached, err := GetCartFromRedis(ctx, userID)
if err != nil {
return nil, err
}
if cached != nil {
if err := RefreshCartTTL(ctx, userID); err != nil {
log.Printf("Warning: Failed to refresh cart TTL for user %s: %v", userID, err)
}
return s.buildResponseFromCache(ctx, userID, cached)
}
cart, err := s.repo.GetCartByUser(ctx, userID)
if err != nil {
return &dto.ResponseCartDTO{
ID: "",
UserID: userID,
TotalAmount: 0,
EstimatedTotalPrice: 0,
CartItems: []dto.ResponseCartItemDTO{},
}, nil
}
response := s.buildResponseFromDB(cart)
cacheData := dto.RequestCartDTO{CartItems: []dto.RequestCartItemDTO{}}
for _, item := range cart.CartItems {
cacheData.CartItems = append(cacheData.CartItems, dto.RequestCartItemDTO{
TrashID: item.TrashCategoryID,
Amount: item.Amount,
})
}
if err := SetCartToRedis(ctx, userID, cacheData); err != nil {
log.Printf("Warning: Failed to cache cart for user %s: %v", userID, err)
}
return response, nil
}
func (s *cartService) DeleteItem(ctx context.Context, userID string, trashID string) error {
return RemoveCartItemFromRedis(ctx, userID, trashID)
existingCart, err := GetCartFromRedis(ctx, userID)
if err != nil {
return err
}
if existingCart == nil {
return errors.New("keranjang tidak ditemukan")
}
filtered := []dto.RequestCartItemDTO{}
for _, item := range existingCart.CartItems {
if item.TrashID != trashID {
filtered = append(filtered, item)
}
}
existingCart.CartItems = filtered
return SetCartToRedis(ctx, userID, *existingCart)
}
func (s *cartService) ClearCart(ctx context.Context, userID string) error {
return DeleteCartFromRedis(ctx, userID)
}
if err := DeleteCartFromRedis(ctx, userID); err != nil {
return err
}
return s.repo.DeleteCart(ctx, userID)
}
func (s *cartService) Checkout(ctx context.Context, userID string) error {
cachedCart, err := GetCartFromRedis(ctx, userID)
if err != nil {
return err
}
if cachedCart != nil {
if err := s.commitCartFromRedis(ctx, userID, cachedCart); err != nil {
return err
}
}
_, err = s.repo.GetCartByUser(ctx, userID)
if err != nil {
return err
}
DeleteCartFromRedis(ctx, userID)
return s.repo.DeleteCart(ctx, userID)
}
func (s *cartService) buildResponseFromCache(ctx context.Context, userID string, cached *dto.RequestCartDTO) (*dto.ResponseCartDTO, error) {
totalQty := 0.0
totalPrice := 0.0
items := []dto.ResponseCartItemDTO{}
for _, item := range cached.CartItems {
trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID)
if err != nil {
log.Printf("Warning: Trash category %s not found for cached cart item", item.TrashID)
continue
}
subtotal := item.Amount * trash.EstimatedPrice
totalQty += item.Amount
totalPrice += subtotal
items = append(items, dto.ResponseCartItemDTO{
ID: "",
TrashID: item.TrashID,
TrashName: trash.Name,
TrashIcon: trash.Icon,
TrashPrice: trash.EstimatedPrice,
Amount: item.Amount,
SubTotalEstimatedPrice: subtotal,
})
}
return &dto.ResponseCartDTO{
ID: "-",
UserID: userID,
TotalAmount: totalQty,
EstimatedTotalPrice: totalPrice,
CartItems: items,
}, nil
}
func (s *cartService) buildResponseFromDB(cart *model.Cart) *dto.ResponseCartDTO {
var items []dto.ResponseCartItemDTO
for _, item := range cart.CartItems {
items = append(items, dto.ResponseCartItemDTO{
ID: item.ID,
TrashID: item.TrashCategoryID,
TrashName: item.TrashCategory.Name,
TrashIcon: item.TrashCategory.Icon,
TrashPrice: item.TrashCategory.EstimatedPrice,
Amount: item.Amount,
SubTotalEstimatedPrice: item.SubTotalEstimatedPrice,
})
}
return &dto.ResponseCartDTO{
ID: cart.ID,
UserID: cart.UserID,
TotalAmount: cart.TotalAmount,
EstimatedTotalPrice: cart.EstimatedTotalPrice,
CartItems: items,
}
}
func (s *cartService) commitCartFromRedis(ctx context.Context, userID string, cachedCart *dto.RequestCartDTO) error {
if len(cachedCart.CartItems) == 0 {
return nil
}
totalAmount := 0.0
totalPrice := 0.0
var cartItems []model.CartItem
for _, item := range cachedCart.CartItems {
trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID)
if err != nil {
log.Printf("Warning: Skipping invalid trash category %s during commit", item.TrashID)
continue
}
subtotal := item.Amount * trash.EstimatedPrice
totalAmount += item.Amount
totalPrice += subtotal
cartItems = append(cartItems, model.CartItem{
TrashCategoryID: item.TrashID,
Amount: item.Amount,
SubTotalEstimatedPrice: subtotal,
})
}
if len(cartItems) == 0 {
return nil
}
newCart := &model.Cart{
UserID: userID,
TotalAmount: totalAmount,
EstimatedTotalPrice: totalPrice,
CartItems: cartItems,
}
return s.repo.CreateCartWithItems(ctx, newCart)
}

View File

@ -67,8 +67,6 @@ func (s *requestPickupService) ConvertCartToRequestPickup(ctx context.Context, u
Notes: req.Notes,
StatusPickup: "waiting_collector",
RequestItems: requestItems,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.pickupRepo.CreateRequestPickup(ctx, &pickup); err != nil {

View File

@ -3,109 +3,156 @@ package worker
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"time"
"rijig/config"
"rijig/dto"
"rijig/internal/repositories"
"rijig/internal/services"
"rijig/model"
)
func CommitExpiredCartsToDB() error {
type CartWorker struct {
cartService services.CartService
cartRepo repositories.CartRepository
trashRepo repositories.TrashRepository
}
func NewCartWorker(cartService services.CartService, cartRepo repositories.CartRepository, trashRepo repositories.TrashRepository) *CartWorker {
return &CartWorker{
cartService: cartService,
cartRepo: cartRepo,
trashRepo: trashRepo,
}
}
func (w *CartWorker) AutoCommitExpiringCarts() error {
ctx := context.Background()
threshold := 1 * time.Minute
keys, err := config.RedisClient.Keys(ctx, "cart:user:*").Result()
keys, err := services.GetExpiringCartKeys(ctx, threshold)
if err != nil {
return fmt.Errorf("error fetching cart keys: %w", err)
return err
}
if len(keys) == 0 {
return nil
}
log.Printf("[CART-WORKER] Found %d carts expiring within 1 minute", len(keys))
successCount := 0
for _, key := range keys {
ttl, err := config.RedisClient.TTL(ctx, key).Result()
if err != nil || ttl > 30*time.Second {
userID := w.extractUserIDFromKey(key)
if userID == "" {
log.Printf("[CART-WORKER] Invalid key format: %s", key)
continue
}
val, err := config.RedisClient.Get(ctx, key).Result()
hasCart, err := w.cartRepo.HasExistingCart(ctx, userID)
if err != nil {
log.Printf("[CART-WORKER] Error checking existing cart for user %s: %v", userID, err)
continue
}
var cart dto.RequestCartDTO
if err := json.Unmarshal([]byte(val), &cart); err != nil {
if hasCart {
if err := services.DeleteCartFromRedis(ctx, userID); err != nil {
log.Printf("[CART-WORKER] Failed to delete Redis cache for user %s: %v", userID, err)
} else {
log.Printf("[CART-WORKER] Deleted Redis cache for user %s (already has DB cart)", userID)
}
continue
}
userID := extractUserIDFromKey(key)
cartData, err := w.getCartFromRedis(ctx, key)
if err != nil {
log.Printf("[CART-WORKER] Failed to get cart data for key %s: %v", key, err)
continue
}
cartID := SaveCartToDB(ctx, userID, &cart)
if err := w.commitCartToDB(ctx, userID, cartData); err != nil {
log.Printf("[CART-WORKER] Failed to commit cart for user %s: %v", userID, err)
continue
}
_ = 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 := services.DeleteCartFromRedis(ctx, userID); err != nil {
log.Printf("[CART-WORKER] Warning: Failed to delete Redis key after commit for user %s: %v", userID, err)
}
successCount++
log.Printf("[CART-WORKER] Successfully auto-committed cart for user %s", userID)
}
log.Printf("[CART-WORKER] Auto-commit completed: %d successful commits", successCount)
return nil
}
func extractUserIDFromKey(key string) string {
func (w *CartWorker) extractUserIDFromKey(key string) string {
parts := strings.Split(key, ":")
if len(parts) == 3 {
return parts[2]
if len(parts) >= 2 {
return parts[len(parts)-1]
}
return ""
}
func SaveCartToDB(ctx context.Context, userID string, cart *dto.RequestCartDTO) string {
totalAmount := float32(0)
totalPrice := float32(0)
func (w *CartWorker) getCartFromRedis(ctx context.Context, key string) (*dto.RequestCartDTO, error) {
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, err
}
return &cart, nil
}
func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData *dto.RequestCartDTO) error {
if len(cartData.CartItems) == 0 {
return nil
}
totalAmount := 0.0
totalPrice := 0.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 {
for _, item := range cartData.CartItems {
if item.Amount <= 0 {
continue
}
subtotal := trash.EstimatedPrice * float64(item.Amount)
trash, err := w.trashRepo.GetTrashCategoryByID(ctx, item.TrashID)
if err != nil {
log.Printf("[CART-WORKER] Warning: Skipping invalid trash category %s", item.TrashID)
continue
}
subtotal := item.Amount * trash.EstimatedPrice
totalAmount += item.Amount
totalPrice += float32(subtotal)
totalPrice += subtotal
cartItems = append(cartItems, model.CartItem{
TrashCategoryID: item.TrashID,
Amount: item.Amount,
SubTotalEstimatedPrice: float32(subtotal),
SubTotalEstimatedPrice: subtotal,
})
}
newCart := model.Cart{
if len(cartItems) == 0 {
return nil
}
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
return w.cartRepo.CreateCartWithItems(ctx, newCart)
}

View File

@ -9,8 +9,8 @@ type Cart struct {
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:"cart_items"`
TotalAmount float32 `json:"total_amount"`
EstimatedTotalPrice float32 `json:"estimated_total_price"`
TotalAmount float64 `json:"total_amount"`
EstimatedTotalPrice float64 `json:"estimated_total_price"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
@ -21,8 +21,8 @@ type CartItem struct {
Cart *Cart `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"-"`
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:"subtotal_estimated_price"`
Amount float64 `json:"amount"`
SubTotalEstimatedPrice float64 `json:"subtotal_estimated_price"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}

View File

@ -1,7 +1,9 @@
package presentation
import (
"rijig/config"
"rijig/internal/handler"
"rijig/internal/repositories"
"rijig/internal/services"
"rijig/middleware"
@ -9,16 +11,18 @@ import (
)
func TrashCartRouter(api fiber.Router) {
cartService := services.NewCartService()
repo := repositories.NewCartRepository()
trashRepo := repositories.NewTrashRepository(config.DB)
cartService := services.NewCartService(repo, trashRepo)
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)
cart.Post("/item", cartHandler.AddOrUpdateItem)
cart.Delete("/item/:trash_id", cartHandler.DeleteItem)
cart.Delete("/clear", cartHandler.ClearCart)
}
// cart.Post("/items", cartHandler.AddMultipleCartItems)

View File

@ -11,11 +11,13 @@ import (
)
func CollectorRouter(api fiber.Router) {
cartRepo := repositories.NewCartRepository()
// trashRepo repositories.TrashRepository
pickupRepo := repositories.NewRequestPickupRepository()
trashRepo := repositories.NewTrashRepository(config.DB)
historyRepo := repositories.NewPickupStatusHistoryRepository()
cartService := services.NewCartService()
cartService := services.NewCartService(cartRepo, trashRepo)
pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo)
pickupHandler := handler.NewRequestPickupHandler(pickupService)

View File

@ -11,10 +11,12 @@ import (
)
func RequestPickupRouter(api fiber.Router) {
cartRepo := repositories.NewCartRepository()
pickupRepo := repositories.NewRequestPickupRepository()
historyRepo := repositories.NewPickupStatusHistoryRepository()
trashRepo := repositories.NewTrashRepository(config.DB)
cartService := services.NewCartService()
cartService := services.NewCartService(cartRepo, trashRepo)
historyService := services.NewPickupStatusHistoryService(historyRepo)
pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo)

View File

@ -5,9 +5,8 @@ import (
"encoding/json"
"fmt"
"log"
"time"
"rijig/config"
"time"
"github.com/go-redis/redis/v8"
)