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" ) const ( FieldNumber = 5 UserIDField = "user_id" CreatedAtField = "created_at" ExpireAtField = "expire_at" TotalPriceField = "total_price" ) type Config struct { KartKeyPrefix string `koanf:"kart_key_prefix"` TTL time.Duration `koanf:"ttl"` } type Repo struct { client *redis.Client config Config } func New(client *redis.Client, cfg Config) Repo { return Repo{client: client, config: cfg} } func (r Repo) cartKey(userID types.ID) string { return r.config.KartKeyPrefix + fmt.Sprintf("%d", userID) } func (r Repo) itemKey(productID types.ID) string { return fmt.Sprintf("item:%d", productID) } 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.TTL.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.TTL.Nanoseconds()) if err := r.updateTotalPrice(ctx, cartKey); err != nil { return err } } r.client.Expire(ctx, cartKey, r.config.TTL) 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.existsCart" 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 }