fix: fixing login worker and flow logic
This commit is contained in:
parent
0467598e58
commit
3f1e0a96ca
|
@ -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)
|
|
||||||
// }
|
|
||||||
// }
|
|
85
cmd/main.go
85
cmd/main.go
|
@ -1,14 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"rijig/config"
|
"rijig/config"
|
||||||
"rijig/internal/repositories"
|
|
||||||
"rijig/internal/services"
|
|
||||||
"rijig/router"
|
"rijig/router"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
@ -17,67 +11,36 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
config.SetupConfig()
|
config.SetupConfig()
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
|
||||||
app.Use(cors.New(cors.Config{
|
app.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: "*",
|
AllowOrigins: "*",
|
||||||
AllowMethods: "GET,POST,PUT,PATCH,DELETE",
|
AllowMethods: "GET,POST,PUT,PATCH,DELETE",
|
||||||
AllowHeaders: "Content-Type,x-api-key",
|
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)
|
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)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -42,10 +42,6 @@ func (r *RequestTrashCategoryDTO) ValidateTrashCategoryInput() (map[string][]str
|
||||||
errors["name"] = append(errors["name"], "name is required")
|
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 {
|
if len(errors) > 0 {
|
||||||
return errors, false
|
return errors, false
|
||||||
}
|
}
|
||||||
|
@ -60,10 +56,6 @@ func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string,
|
||||||
errors["description"] = append(errors["description"], "description is required")
|
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 {
|
if len(errors) > 0 {
|
||||||
return errors, false
|
return errors, false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ValidationErrors struct {
|
type ValidationErrors struct {
|
||||||
|
@ -19,8 +19,8 @@ type CartResponse struct {
|
||||||
CartItems []CartItemResponse `json:"cartitems"`
|
CartItems []CartItemResponse `json:"cartitems"`
|
||||||
TotalAmount float32 `json:"totalamount"`
|
TotalAmount float32 `json:"totalamount"`
|
||||||
EstimatedTotalPrice float32 `json:"estimated_totalprice"`
|
EstimatedTotalPrice float32 `json:"estimated_totalprice"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CartItemResponse struct {
|
type CartItemResponse struct {
|
||||||
|
@ -48,3 +48,20 @@ func (r *RequestCartItems) ValidateRequestCartItem() (map[string][]string, bool)
|
||||||
|
|
||||||
return nil, true
|
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
|
||||||
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -37,7 +37,6 @@ require (
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/mdp/qrterminal/v3 v3.2.0
|
github.com/mdp/qrterminal/v3 v3.2.0
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
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/rs/zerolog v1.33.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
|
|
|
@ -9,88 +9,88 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type CartHandler struct {
|
type CartHandler struct {
|
||||||
CartService services.CartService
|
Service *services.CartService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCartHandler(service services.CartService) *CartHandler {
|
func NewCartHandler(service *services.CartService) *CartHandler {
|
||||||
return &CartHandler{
|
return &CartHandler{Service: service}
|
||||||
CartService: service,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /cart - Get cart by user ID
|
func (h *CartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error {
|
||||||
func (h *CartHandler) GetCart(c *fiber.Ctx) error {
|
var body dto.BulkRequestCartItems
|
||||||
userID, ok := c.Locals("userID").(string)
|
if err := c.BodyParser(&body); err != nil {
|
||||||
if !ok || userID == "" {
|
return utils.ValidationErrorResponse(c, map[string][]string{
|
||||||
return utils.ErrorResponse(c, "unauthorized or invalid user")
|
"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 {
|
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, "Item deleted")
|
||||||
return utils.SuccessResponse(c, nil, "Cart is empty")
|
}
|
||||||
|
|
||||||
|
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")
|
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 {
|
func (h *CartHandler) CommitCart(c *fiber.Ctx) error {
|
||||||
userID, ok := c.Locals("userID").(string)
|
userID := c.Locals("userID").(string)
|
||||||
if !ok || userID == "" {
|
|
||||||
return utils.ErrorResponse(c, "unauthorized or invalid user")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := h.CartService.CommitCartFromRedis(userID)
|
err := h.Service.CommitCartToDatabase(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "cart not found in redis" {
|
return utils.InternalServerErrorResponse(c, "Failed to commit cart to database")
|
||||||
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")
|
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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ type TrashRepository interface {
|
||||||
AddDetailToCategory(detail *model.TrashDetail) error
|
AddDetailToCategory(detail *model.TrashDetail) error
|
||||||
GetCategories() ([]model.TrashCategory, error)
|
GetCategories() ([]model.TrashCategory, error)
|
||||||
GetCategoryByID(id string) (*model.TrashCategory, error)
|
GetCategoryByID(id string) (*model.TrashCategory, error)
|
||||||
|
FindCategoryId(id string) (*model.TrashCategory, error)
|
||||||
GetTrashCategoryByName(name string) (*model.TrashCategory, error)
|
GetTrashCategoryByName(name string) (*model.TrashCategory, error)
|
||||||
GetTrashDetailByID(id string) (*model.TrashDetail, error)
|
GetTrashDetailByID(id string) (*model.TrashDetail, error)
|
||||||
GetDetailsByCategoryID(categoryID 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
|
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) {
|
func (r *trashRepository) GetTrashCategoryByName(name string) (*model.TrashCategory, error) {
|
||||||
var category model.TrashCategory
|
var category model.TrashCategory
|
||||||
|
|
||||||
|
|
|
@ -1,100 +1,46 @@
|
||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"rijig/config"
|
"rijig/config"
|
||||||
"rijig/model"
|
"rijig/model"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CartRepository interface {
|
type CartRepository interface {
|
||||||
Create(cart *model.Cart) error
|
CreateCart(cart *model.Cart) error
|
||||||
GetByUserID(userID string) (*model.Cart, error)
|
GetTrashCategoryByID(id string) (*model.TrashCategory, error)
|
||||||
Update(cart *model.Cart) error
|
GetCartByUserID(userID string) (*model.Cart, error)
|
||||||
InsertCartItem(item *model.CartItem) error
|
DeleteCartByUserID(userID string) error
|
||||||
UpdateCartItem(item *model.CartItem) error
|
|
||||||
DeleteCartItemByID(id string) error
|
|
||||||
Delete(cartID string) error
|
|
||||||
DeleteByUserID(userID string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type cartRepository struct {
|
type cartRepository struct{}
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCartRepository() CartRepository {
|
func NewCartRepository() CartRepository {
|
||||||
return &cartRepository{
|
return &cartRepository{}
|
||||||
db: config.DB,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *cartRepository) Create(cart *model.Cart) error {
|
func (r *cartRepository) CreateCart(cart *model.Cart) error {
|
||||||
tx := r.db.Begin()
|
return config.DB.Create(cart).Error
|
||||||
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) {
|
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
|
var cart model.Cart
|
||||||
|
err := config.DB.Preload("CartItems.TrashCategory").
|
||||||
err := r.db.
|
|
||||||
Preload("CartItems.TrashCategory").
|
|
||||||
Where("user_id = ?", userID).
|
Where("user_id = ?", userID).
|
||||||
First(&cart).Error
|
First(&cart).Error
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return nil, err
|
||||||
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
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,130 +1,135 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"rijig/dto"
|
"rijig/dto"
|
||||||
"rijig/internal/repositories"
|
"rijig/internal/repositories"
|
||||||
"rijig/model"
|
"rijig/model"
|
||||||
"rijig/utils"
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CartService interface {
|
type CartService struct {
|
||||||
CreateCartFromDTO(userID string, items []dto.RequestCartItems) error
|
Repo repositories.CartRepository
|
||||||
GetCartByUserID(userID string) (*dto.CartResponse, error)
|
|
||||||
CommitCartFromRedis(userID string) error
|
|
||||||
DeleteCart(cartID string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type cartService struct {
|
func NewCartService(repo repositories.CartRepository) *CartService {
|
||||||
repo repositories.CartRepository
|
return &CartService{Repo: repo}
|
||||||
repoTrash repositories.TrashRepository
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCartService(repo repositories.CartRepository, repoTrash repositories.TrashRepository) CartService {
|
func (s *CartService) CommitCartToDatabase(userID string) error {
|
||||||
return &cartService{repo: repo, repoTrash: repoTrash}
|
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 {
|
var cartItems []model.CartItem
|
||||||
return fmt.Sprintf("cart:user:%s", userID)
|
var totalAmount float32
|
||||||
}
|
var estimatedTotal float32
|
||||||
|
|
||||||
func (s *cartService) CreateCartFromDTO(userID string, items []dto.RequestCartItems) error {
|
|
||||||
// Validasi semua item
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if errMap, valid := item.ValidateRequestCartItem(); !valid {
|
trash, err := s.Repo.GetTrashCategoryByID(item.TrashID)
|
||||||
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to retrieve trash category for id %s: %v", input.TrashID, err)
|
log.Printf("Trash category not found for trashID: %s", item.TrashID)
|
||||||
}
|
|
||||||
|
|
||||||
if input.Amount == 0 {
|
|
||||||
delete(itemMap, trash.Name) // hapus item
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
subtotal := float32(trash.EstimatedPrice) * input.Amount
|
subTotal := float32(trash.EstimatedPrice) * item.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
|
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{
|
cart := &model.Cart{
|
||||||
ID: existingCart.ID,
|
ID: uuid.NewString(),
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
CartItems: cartItems,
|
||||||
TotalAmount: totalAmount,
|
TotalAmount: totalAmount,
|
||||||
EstimatedTotalPrice: totalPrice,
|
EstimatedTotalPrice: estimatedTotal,
|
||||||
CartItems: finalItems,
|
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simpan ulang ke Redis dengan TTL 10 menit
|
if err := s.Repo.DeleteCartByUserID(userID); err != nil {
|
||||||
return utils.SetData(redisCartKey(userID), cart, 1*time.Minute)
|
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) GetCartFromRedis(userID string) (*dto.CartResponse, error) {
|
||||||
func (s *cartService) GetCartByUserID(userID string) (*dto.CartResponse, error) {
|
items, err := GetCartItems(userID)
|
||||||
val, err := utils.GetData(redisCartKey(userID))
|
if err != nil || len(items) == 0 {
|
||||||
if err != nil {
|
return nil, err
|
||||||
log.Printf("Redis get error: %v", err)
|
|
||||||
}
|
}
|
||||||
if val != "" {
|
|
||||||
var cached dto.CartResponse
|
var totalAmount float32
|
||||||
if err := json.Unmarshal([]byte(val), &cached); err == nil {
|
var estimatedTotal float32
|
||||||
return &cached, nil
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if cart == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var items []dto.CartItemResponse
|
var items []dto.CartItemResponse
|
||||||
for _, item := range cart.CartItems {
|
for _, item := range cartDB.CartItems {
|
||||||
items = append(items, dto.CartItemResponse{
|
items = append(items, dto.CartItemResponse{
|
||||||
TrashIcon: item.TrashCategory.Icon,
|
TrashIcon: item.TrashCategory.Icon,
|
||||||
TrashName: item.TrashCategory.Name,
|
TrashName: item.TrashCategory.Name,
|
||||||
|
@ -133,108 +138,14 @@ func (s *cartService) GetCartByUserID(userID string) (*dto.CartResponse, error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &dto.CartResponse{
|
resp := &dto.CartResponse{
|
||||||
ID: cart.ID,
|
ID: cartDB.ID,
|
||||||
UserID: cart.UserID,
|
UserID: cartDB.UserID,
|
||||||
TotalAmount: cart.TotalAmount,
|
TotalAmount: cartDB.TotalAmount,
|
||||||
EstimatedTotalPrice: cart.EstimatedTotalPrice,
|
EstimatedTotalPrice: cartDB.EstimatedTotalPrice,
|
||||||
|
CreatedAt: cartDB.CreatedAt.Format(time.RFC3339),
|
||||||
|
UpdatedAt: cartDB.UpdatedAt.Format(time.RFC3339),
|
||||||
CartItems: items,
|
CartItems: items,
|
||||||
CreatedAt: cart.CreatedAt,
|
|
||||||
UpdatedAt: cart.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
|
return resp, nil
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 ""
|
||||||
|
}
|
|
@ -35,6 +35,10 @@ func AuthMiddleware(c *fiber.Ctx) error {
|
||||||
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims")
|
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)
|
userID := claims["sub"].(string)
|
||||||
deviceID := claims["device_id"].(string)
|
deviceID := claims["device_id"].(string)
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
package presentation
|
package presentation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"rijig/config"
|
|
||||||
"rijig/internal/handler"
|
"rijig/internal/handler"
|
||||||
"rijig/internal/repositories"
|
"rijig/internal/repositories"
|
||||||
"rijig/internal/services"
|
"rijig/internal/services"
|
||||||
|
"rijig/internal/worker"
|
||||||
"rijig/middleware"
|
"rijig/middleware"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TrashCartRouter(api fiber.Router) {
|
func TrashCartRouter(api fiber.Router) {
|
||||||
|
|
||||||
cartRepo := repositories.NewCartRepository()
|
cartRepo := repositories.NewCartRepository()
|
||||||
trashRepo := repositories.NewTrashRepository(config.DB)
|
cartService := services.NewCartService(cartRepo)
|
||||||
cartService := services.NewCartService(cartRepo, trashRepo)
|
|
||||||
cartHandler := handler.NewCartHandler(cartService)
|
cartHandler := handler.NewCartHandler(cartService)
|
||||||
|
|
||||||
cart := api.Group("/cart")
|
worker.StartCartCommitWorker(cartService)
|
||||||
cart.Use(middleware.AuthMiddleware)
|
|
||||||
cart.Post("/", cartHandler.CreateCart)
|
cart := api.Group("/cart", middleware.AuthMiddleware)
|
||||||
|
cart.Put("/refresh", cartHandler.RefreshCartTTL)
|
||||||
|
cart.Post("/", cartHandler.AddOrUpdateCartItem)
|
||||||
cart.Get("/", cartHandler.GetCart)
|
cart.Get("/", cartHandler.GetCart)
|
||||||
cart.Post("/commit", cartHandler.CommitCart)
|
cart.Post("/commit", cartHandler.CommitCart)
|
||||||
cart.Delete("/:id", cartHandler.DeleteCart)
|
cart.Delete("/", cartHandler.ClearCart)
|
||||||
|
cart.Delete("/:trashid", cartHandler.DeleteCartItem)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue