package repository import ( "context" "encoding/json" "fmt" adapter "git.gocasts.ir/ebhomengo/niki/adapter/redis" "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" "time" ) type Cache struct { client *adapter.Adapter config Config } func NewCache(client *adapter.Adapter, cfg Config) Cache { return Cache{client: client, config: cfg} } func (c Cache) cartKey(userID types.ID) string { return fmt.Sprintf("%s:%d", c.config.CacheKartKeyPrefix, userID) } func (c Cache) itemKey(itemID types.ID) string { return fmt.Sprintf("item:%d", itemID) } func (c Cache) kartIDsKey(userID types.ID) string { return fmt.Sprintf("cart:%d:items", userID) } func (c Cache) upsertCart(ctx context.Context, cart entity.Cart) error { cartKey := c.cartKey(cart.UserID) cartIDsKey := c.kartIDsKey(cart.UserID) pipe := c.client.Client().Pipeline() pipe.HSet(ctx, cartKey, map[string]interface{}{ IDField: cart.ID, UserIDField: cart.UserID, StatusField: cart.Status, TotalPriceField: cart.TotalPrice, ExpireAtField: cart.ExpireAt, CreatedAtField: cart.CreatedAt, UpdatedAtField: cart.UpdatedAt, }) pipe.Expire(ctx, cartKey, c.config.CacheTTL) pipe.Del(ctx, cartIDsKey) for _, i := range cart.Items { pipe.SAdd(ctx, cartIDsKey, i.ID) itemKey := c.itemKey(i.ID) jsonItem, err := json.Marshal(i) if err != nil { return richerror.New(richerror.Op(op("cache.upsertCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } pipe.Set(ctx, itemKey, string(jsonItem), c.config.CacheTTL) } pipe.Expire(ctx, cartIDsKey, c.config.CacheTTL) _, err := pipe.Exec(ctx) if err != nil { return richerror.New(richerror.Op(op("cache.upsertCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } return nil } func (c Cache) getCart(ctx context.Context, userID types.ID) (entity.Cart, error) { cartKey := c.cartKey(userID) cartIDsKey := c.kartIDsKey(userID) pipe := c.client.Client().Pipeline() resCart := pipe.HGetAll(ctx, cartKey) resIDs := pipe.SMembers(ctx, cartIDsKey) _, err := pipe.Exec(ctx) if err != nil { return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } result := resCart.Val() if len(result) < 1 { return entity.Cart{}, nil } cart, err := parseCartFromRedis(result) if err != nil { return entity.Cart{}, err } if cart.ID < 1 { return entity.Cart{}, nil } ids := resIDs.Val() if len(ids) < 1 { return cart, nil } itemPipe := c.client.Client().Pipeline() itemResults := make([]*redis.StringCmd, len(ids)) for i, idStr := range ids { id, _ := strconv.Atoi(idStr) itemKey := c.itemKey(types.ID(id)) cmd := itemPipe.Get(ctx, itemKey) itemResults[i] = cmd } if _, err := itemPipe.Exec(ctx); err != nil { return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } for _, cmd := range itemResults { if err := cmd.Err(); err != nil { if err == redis.Nil { continue } return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } var item entity.Item if err := json.Unmarshal([]byte(cmd.Val()), &item); err != nil { return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } cart.Items = append(cart.Items, item) } return cart, nil } func parseCartFromRedis(result map[string]string) (entity.Cart, error) { if len(result) == 0 { return entity.Cart{}, nil } var cart entity.Cart cart.Items = []entity.Item{} for key, val := range result { switch key { case IDField: id, err := strconv.ParseInt(val, 10, 64) if err != nil { return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } cart.ID = types.ID(id) case UserIDField: uid, err := strconv.ParseInt(val, 10, 64) if err != nil { return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } cart.UserID = types.ID(uid) case StatusField: cart.Status = entity.CartStatus(val) case TotalPriceField: price, err := strconv.ParseFloat(val, 64) if err != nil { return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } cart.TotalPrice = price case ExpireAtField, CreatedAtField, UpdatedAtField: t, err := time.Parse(time.RFC3339, val) if err != nil { return entity.Cart{}, richerror.New(richerror.Op(op("cache.getCart"))). WithKind(richerror.KindUnexpected).WithErr(err) } switch key { case ExpireAtField: cart.ExpireAt = t case CreatedAtField: cart.CreatedAt = t case UpdatedAtField: cart.UpdatedAt = t } } } return cart, nil }