diff --git a/domain/shoppingbasket/repository/adapter.go b/domain/shoppingbasket/repository/adapter.go new file mode 100644 index 00000000..faad6561 --- /dev/null +++ b/domain/shoppingbasket/repository/adapter.go @@ -0,0 +1,123 @@ +package repository + +import ( + "context" + "fmt" + "git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity" + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) + +type Config struct { + CacheKartKeyPrefix string `koanf:"cache_kart_key_prefix"` + CacheTTL time.Duration `koanf:"cache_ttl"` + + MysqlTTL time.Duration `koanf:"mysql_ttl"` +} + +type Repo struct { + db DB + cache Cache +} + +func New(db DB, cache Cache) Repo { + return Repo{db: db, cache: cache} +} + +func op(s string) string { + return fmt.Sprintf("shoppingbasketapp-repository-", s) +} + +func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error { + if err := r.db.addToBasket(ctx, userID, item); err != nil { + return err + } + + cart, err := r.db.getCart(ctx, nil, FindCartByUserIDQuery, userID) + if err != nil { + return err + } + + return r.cache.upsertCart(ctx, cart) +} + +func (r Repo) GetCart(ctx context.Context, userID types.ID) (entity.Cart, error) { + cart, err := r.cache.getCart(ctx, userID) + if err != nil { + return entity.Cart{}, err + } + + if cart.ID < 1 || len(cart.Items) < 1 { + c, err := r.db.getCart(ctx, nil, FindCartByUserIDQuery, userID) + if err != nil { + return entity.Cart{}, err + } + + if c.ID < 1 { + return entity.Cart{}, nil + } + + if err := r.cache.upsertCart(ctx, c); err != nil { + return entity.Cart{}, err + } + + return c, nil + } + + return cart, nil +} + +func (r Repo) UpdateQuantity(ctx context.Context, cartID, itemID types.ID, quantity int) error { + + cart, err := r.db.getCart(ctx, nil, FindCartByIDQuery, cartID) + if err != nil { + return err + } + + for i, item := range cart.Items { + if item.ID == itemID { + cart.Items[i].Quantity = quantity + item.Quantity = quantity + if err := r.db.updateQuantity(ctx, nil, item); err != nil { + return err + } + } + } + + return r.cache.upsertCart(ctx, cart) +} + +func (r Repo) UpdateStatus(ctx context.Context, cartID types.ID, status entity.CartStatus) error { + if err := r.db.updateCartStatus(ctx, nil, cartID, status); err != nil { + return err + } + + cart, err := r.db.getCart(ctx, nil, FindCartByIDQuery, cartID) + if err != nil { + return err + } + + return r.cache.upsertCart(ctx, cart) +} + +func (r Repo) DeleteItem(ctx context.Context, cartID, itemID types.ID) error { + if err := r.db.deleteItem(ctx, nil, cartID, itemID); err != nil { + return err + } + + c, err := r.db.getCart(ctx, nil, FindCartByIDQuery, cartID) + if err != nil { + return err + } + + return r.cache.upsertCart(ctx, c) +} + +func (r Repo) DeleteCart(ctx context.Context, cartID, userID types.ID) error { + + if err := r.db.deleteCart(ctx, cartID, userID); err != nil { + return err + } + + return r.cache.deleteCart(ctx, userID) +} diff --git a/domain/shoppingbasket/repository/cache.go b/domain/shoppingbasket/repository/cache.go index 41a8cb6a..8518b339 100644 --- a/domain/shoppingbasket/repository/cache.go +++ b/domain/shoppingbasket/repository/cache.go @@ -207,3 +207,20 @@ func parseCartFromRedis(result map[string]string) (entity.Cart, error) { return cart, nil } + +func (c Cache) deleteCart(ctx context.Context, userID types.ID) error { + cartKey := c.cartKey(userID) + cartIDsKey := c.kartIDsKey(userID) + pipe := c.client.Client().Pipeline() + + pipe.Del(ctx, cartKey) + pipe.Del(ctx, cartIDsKey) + + _, err := pipe.Exec(ctx) + if err != nil { + return richerror.New(richerror.Op(op("cache-deleteCart"))). + WithKind(richerror.KindUnexpected).WithErr(err) + } + + return nil +} diff --git a/domain/shoppingbasket/repository/cart.go b/domain/shoppingbasket/repository/cart.go deleted file mode 100644 index 3e6923ff..00000000 --- a/domain/shoppingbasket/repository/cart.go +++ /dev/null @@ -1,262 +0,0 @@ -package repository - -import ( - "context" - "encoding/json" - "fmt" - "git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity" - richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" - "git.gocasts.ir/ebhomengo/niki/types" - "github.com/redis/go-redis/v9" - "strconv" - "strings" - "time" -) - -type Config struct { - CacheKartKeyPrefix string `koanf:"cache_kart_key_prefix"` - CacheTTL time.Duration `koanf:"cache_ttl"` - - MysqlTTL time.Duration `koanf:"mysql_ttl"` -} - -type Repo struct { - client *redis.Client - config Config -} - -func New(client *redis.Client, cfg Config) Repo { - return Repo{client: client, config: cfg} -} - -func op(s string) string { - return fmt.Sprintf("shoppingbasketapp-repository-", s) -} - -func (r Repo) AddItem(ctx context.Context, userID types.ID, item entity.Item) error { - const op = "shoppingbasketapp.repository.AddItem" - - cartKey := r.cartKey(userID) - itemKey := r.itemKey(item.ProductID) - now := time.Now().UnixNano() - - itemJson, _ := json.Marshal(item) - - exists, _ := r.client.Exists(ctx, cartKey).Result() - - if exists == 0 { - r.client.HSet(ctx, cartKey, map[string]interface{}{ - UserIDField: userID, - itemKey: string(itemJson), - TotalPriceField: item.Price * types.Price(item.Quantity), - CreatedAtField: now, - ExpireAtField: now + r.config.CacheTTL.Nanoseconds(), - }) - } else { - existsItem, _ := r.client.HGet(ctx, cartKey, itemKey).Result() - if existsItem != "" { - var i entity.Item - if err := json.Unmarshal([]byte(existsItem), &i); err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - item.Quantity += i.Quantity - itemJson, _ = json.Marshal(item) - } - - r.client.HSet(ctx, cartKey, itemKey, string(itemJson)) - r.client.HSet(ctx, cartKey, ExpireAtField, now+r.config.CacheTTL.Nanoseconds()) - if err := r.updateTotalPrice(ctx, cartKey); err != nil { - return err - } - } - - r.client.Expire(ctx, cartKey, r.config.CacheTTL) - - return nil -} - -func parsInt(s string) int64 { - i, _ := strconv.ParseInt(s, 10, 64) - return i -} - -func (r Repo) GetCart(ctx context.Context, userID types.ID) (entity.Cart, error) { - const op = "shoppingbasketapp.repository.GetCart" - cartKey := r.cartKey(userID) - - exists, err := r.client.Exists(ctx, cartKey).Result() - if err != nil { - return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - if exists == 0 { - return entity.Cart{}, richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket") - } - - allCart, err := r.client.HGetAll(ctx, cartKey).Result() - if err != nil { - return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - c := entity.Cart{Items: []entity.Item{}} - - for field, value := range allCart { - if strings.HasPrefix(field, "item:") { - var i entity.Item - if err := json.Unmarshal([]byte(value), &i); err != nil { - return entity.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - c.Items = append(c.Items, i) - - continue - } - - switch field { - case UserIDField: - c.UserID = types.ID(parsInt(value)) - case TotalPriceField: - c.TotalPrice = types.Price(parsInt(value)) - case CreatedAtField: - c.CreatedAt = parsInt(value) - case ExpireAtField: - c.ExpireAt = parsInt(value) - } - } - - return c, nil -} - -func (r Repo) DeleteItem(ctx context.Context, userID, productID types.ID) error { - const op = "shoppingbasketapp.repository.DeleteItem" - cartKey := r.cartKey(userID) - itemKey := r.itemKey(productID) - - if err := r.existsCart(ctx, cartKey); err != nil { - return err - } - - if err := r.existsItem(ctx, cartKey, itemKey); err != nil { - return err - } - - if err := r.client.HDel(ctx, cartKey, itemKey).Err(); err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - num, err := r.client.HLen(ctx, cartKey).Result() - if err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - if num < FieldNumber { - return r.DeleteCart(ctx, userID) - } - - return r.updateTotalPrice(ctx, cartKey) -} - -func (r Repo) UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error { - const op = "shoppingbasketapp.repository.UpdateQuantity" - cartKey := r.cartKey(userID) - itemKey := r.itemKey(productID) - - if err := r.existsCart(ctx, cartKey); err != nil { - return err - } - - if err := r.existsItem(ctx, cartKey, itemKey); err != nil { - return err - } - - data, err := r.client.HGet(ctx, cartKey, itemKey).Result() - if err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - var item entity.Item - if err := json.Unmarshal([]byte(data), &item); err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - item.Quantity = quantity - - j, _ := json.Marshal(item) - - if err := r.client.HSet(ctx, cartKey, itemKey, string(j)).Err(); err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - return r.updateTotalPrice(ctx, cartKey) -} - -func (r Repo) DeleteCart(ctx context.Context, userID types.ID) error { - const op = "shoppingbasketapp.repository.DeleteCart" - cartKey := r.cartKey(userID) - - if err := r.existsCart(ctx, cartKey); err != nil { - return err - } - - if err := r.client.Del(ctx, cartKey).Err(); err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - return nil -} - -func (r Repo) updateTotalPrice(ctx context.Context, cartKey string) error { - const op = "shoppingbasketapp.repository.updateTotalPrice" - - allFields, err := r.client.HGetAll(ctx, cartKey).Result() - if err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - var total types.Price - - for field, value := range allFields { - if strings.HasPrefix(field, "item:") { - var item entity.Item - - if err := json.Unmarshal([]byte(value), &item); err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - total += item.Price * types.Price(item.Quantity) - } - } - - return r.client.HSet(ctx, cartKey, TotalPriceField, int64(total)).Err() -} - -func (r Repo) existsCart(ctx context.Context, cartKey string) error { - const op = "shoppingbasketapp.repository.existsCart" - - exists, err := r.client.Exists(ctx, cartKey).Result() - if err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - if exists == 0 { - return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket") - } - - return nil -} - -func (r Repo) existsItem(ctx context.Context, cartKey, itemKey string) error { - const op = "shoppingbasketapp.repository.existsItem" - - exists, err := r.client.HExists(ctx, cartKey, itemKey).Result() - if err != nil { - return richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) - } - - if !exists { - return richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found product form shopping basket") - } - - return nil -} diff --git a/domain/shoppingbasket/repository/constant.go b/domain/shoppingbasket/repository/constant.go index 7f4add13..1d7357dc 100644 --- a/domain/shoppingbasket/repository/constant.go +++ b/domain/shoppingbasket/repository/constant.go @@ -5,7 +5,8 @@ import "errors" const ( FindCartByUserIDQuery = `SELECT id, user_id, total_price, status, expire_at, created_at FROM carts - WHERE user_id = ? AND status = 'active' AND deleted_at IS NULL` + WHERE user_id = ? AND status = 'active' AND deleted_at IS NULL + ORDER BY created_at DESC` FindCartByIDQuery = `SELECT id, user_id, total_price, status, expire_at, created_at FROM carts diff --git a/domain/shoppingbasket/repository/mysql.go b/domain/shoppingbasket/repository/mysql.go index 1b2334ce..e92e0c45 100644 --- a/domain/shoppingbasket/repository/mysql.go +++ b/domain/shoppingbasket/repository/mysql.go @@ -302,7 +302,7 @@ func (d DB) updateTotalPrice(ctx context.Context, tx *sql.Tx, cartID types.ID) e return nil } -func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, productID types.ID) error { +func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, itemID types.ID) error { t, ok, err := d.getLocalTX(ctx, tx, "mysql-deleteItem") if err != nil { return err @@ -317,9 +317,9 @@ func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, productID types. query := `UPDATE items SET deleted_at = ? - WHERE cart_id = ? AND product_id = ? AND deleted_at IS NULL` + WHERE cart_id = ? AND id = ? AND deleted_at IS NULL` deleteTime := time.Now().UTC() - _, err = t.ExecContext(ctx, query, deleteTime, cartID, productID) + _, err = t.ExecContext(ctx, query, deleteTime, cartID, itemID) if err != nil { return richerror.New(richerror.Op(op("mysql-deleteItem"))). WithKind(richerror.KindUnexpected).WithErr(err) @@ -340,12 +340,27 @@ func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, productID types. } func (d DB) deleteCart(ctx context.Context, cartID, userID types.ID) error { - query := `UPDATE items + itemQuery := `UPDATE items SET deleted_at = ? WHERE cart_id = ? AND user_id = ? AND deleted_at IS NULL` - deleteTime := time.Now().UTC() - _, err := d.db.Conn().ExecContext(ctx, query, deleteTime, cartID, userID) + + deleteItemTime := time.Now().UTC() + + _, err := d.db.Conn().ExecContext(ctx, itemQuery, deleteItemTime, cartID, userID) + if err != nil { + return richerror.New(richerror.Op(op("mysql-deleteCart"))). + WithKind(richerror.KindUnexpected).WithErr(err) + } + + cartQuery := `UPDATE carts + SET + deleted_at = ? + WHERE id = ? AND user_id = ? AND deleted_at IS NULL` + + deleteCartTime := time.Now().UTC() + + _, err = d.db.Conn().ExecContext(ctx, cartQuery, deleteCartTime, cartID, userID) if err != nil { return richerror.New(richerror.Op(op("mysql-deleteCart"))). WithKind(richerror.KindUnexpected).WithErr(err) @@ -370,7 +385,7 @@ func (d DB) updateCartStatus(ctx context.Context, tx *sql.Tx, cartID types.ID, s query := `UPDATE carts SET status = ? - WHERE cart_id = ? AND deleted_at IS NULL` + WHERE id = ? AND status = active AND deleted_at IS NULL` _, err = t.ExecContext(ctx, query, status, cartID) if err != nil { diff --git a/domain/shoppingbasket/service/service.go b/domain/shoppingbasket/service/service.go index 51806f15..90eb7e08 100644 --- a/domain/shoppingbasket/service/service.go +++ b/domain/shoppingbasket/service/service.go @@ -13,8 +13,8 @@ type Repository interface { AddItem(ctx context.Context, userID types.ID, item entity.Item) error GetCart(ctx context.Context, userID types.ID) (entity.Cart, error) DeleteItem(ctx context.Context, cartID, itemID types.ID) error - UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error - DeleteCart(ctx context.Context, cartID types.ID) error + UpdateQuantity(ctx context.Context, cartID, itemID types.ID, quantity int) error + DeleteCart(ctx context.Context, cartID, userID types.ID) error UpdateStatus(ctx context.Context, cartID types.ID, status entity.CartStatus) error }