From c1ed70cf669c8c45de4f365af2065a31cbcd5e3d Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Sun, 12 Apr 2026 21:32:23 +0330 Subject: [PATCH] added repository layer --- shoppingbasketapp/config.go | 10 + shoppingbasketapp/repository/cart.go | 272 ++++++++++++++++++ .../service/{ => cart}/entity.go | 6 +- shoppingbasketapp/service/{ => cart}/param.go | 4 +- .../service/{ => cart}/service.go | 6 +- .../service/{ => cart}/validator.go | 2 +- 6 files changed, 291 insertions(+), 9 deletions(-) create mode 100644 shoppingbasketapp/repository/cart.go rename shoppingbasketapp/service/{ => cart}/entity.go (80%) rename shoppingbasketapp/service/{ => cart}/param.go (93%) rename shoppingbasketapp/service/{ => cart}/service.go (94%) rename shoppingbasketapp/service/{ => cart}/validator.go (99%) diff --git a/shoppingbasketapp/config.go b/shoppingbasketapp/config.go index 8505883b..991d4f84 100644 --- a/shoppingbasketapp/config.go +++ b/shoppingbasketapp/config.go @@ -1 +1,11 @@ package shoppingbasketapp + +import ( + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" +) + +type Config struct { + Redis redis.Config `koanf:"redis" json:"redis"` + Repo repository.Repo `koanf:"repo" json:"repo"` +} diff --git a/shoppingbasketapp/repository/cart.go b/shoppingbasketapp/repository/cart.go new file mode 100644 index 00000000..7e45ce92 --- /dev/null +++ b/shoppingbasketapp/repository/cart.go @@ -0,0 +1,272 @@ +package repository + +import ( + "context" + "encoding/json" + "fmt" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/service/cart" + "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 cart.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 cart.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) (cart.Cart, error) { + const op = "shoppingbasketapp.repository.GetCart" + cartKey := r.cartKey(userID) + + exists, err := r.client.Exists(ctx, cartKey).Result() + if err != nil { + return cart.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + + if exists == 0 { + return cart.Cart{}, richerror.New(op).WithKind(richerror.KindNotFound).WithMessage("not found shopping basket") + } + + allCart, err := r.client.HGetAll(ctx, cartKey).Result() + if err != nil { + return cart.Cart{}, richerror.New(op).WithKind(richerror.KindUnexpected).WithErr(err) + } + + c := cart.Cart{Items: []cart.Item{}} + + for field, value := range allCart { + if strings.HasPrefix(field, "item:") { + var i cart.Item + if err := json.Unmarshal([]byte(value), &i); err != nil { + return cart.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 cart.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 cart.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 +} diff --git a/shoppingbasketapp/service/entity.go b/shoppingbasketapp/service/cart/entity.go similarity index 80% rename from shoppingbasketapp/service/entity.go rename to shoppingbasketapp/service/cart/entity.go index 6f10c37f..f3032daf 100644 --- a/shoppingbasketapp/service/entity.go +++ b/shoppingbasketapp/service/cart/entity.go @@ -1,10 +1,10 @@ -package service +package cart import ( "git.gocasts.ir/ebhomengo/niki/types" ) -type CartItem struct { +type Item struct { ProductID types.ID Quantity int Price types.Price @@ -14,7 +14,7 @@ type CartItem struct { type Cart struct { UserID types.ID - Items []CartItem + Items []Item TotalPrice types.Price ExpireAt int64 CreatedAt int64 diff --git a/shoppingbasketapp/service/param.go b/shoppingbasketapp/service/cart/param.go similarity index 93% rename from shoppingbasketapp/service/param.go rename to shoppingbasketapp/service/cart/param.go index 264c053e..97d8a520 100644 --- a/shoppingbasketapp/service/param.go +++ b/shoppingbasketapp/service/cart/param.go @@ -1,4 +1,4 @@ -package service +package cart import "git.gocasts.ir/ebhomengo/niki/types" @@ -12,7 +12,7 @@ type AddToCartRequest struct { type GetCartResponse struct { UserID types.ID `json:"user_id"` - Items []CartItem `json:"items"` + Items []Item `json:"items"` TotalPrice types.Price `json:"total_price"` CreatedAt int64 `json:"created_at"` ExpireAt int64 `json:"expire_at"` diff --git a/shoppingbasketapp/service/service.go b/shoppingbasketapp/service/cart/service.go similarity index 94% rename from shoppingbasketapp/service/service.go rename to shoppingbasketapp/service/cart/service.go index 59d5a00b..df860b0b 100644 --- a/shoppingbasketapp/service/service.go +++ b/shoppingbasketapp/service/cart/service.go @@ -1,4 +1,4 @@ -package service +package cart import ( "context" @@ -8,7 +8,7 @@ import ( ) type Repository interface { - AddItem(ctx context.Context, userID types.ID, cart CartItem) error + AddItem(ctx context.Context, userID types.ID, item Item) error GetCart(ctx context.Context, userID types.ID) (Cart, error) DeleteItem(ctx context.Context, userID, productID types.ID) error UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error @@ -31,7 +31,7 @@ func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error { return err } - return s.repo.AddItem(ctx, req.UserID, CartItem{ + return s.repo.AddItem(ctx, req.UserID, Item{ ProductID: req.ProductID, Quantity: req.Quantity, Price: req.Price, diff --git a/shoppingbasketapp/service/validator.go b/shoppingbasketapp/service/cart/validator.go similarity index 99% rename from shoppingbasketapp/service/validator.go rename to shoppingbasketapp/service/cart/validator.go index 8ab07773..2ce4b2f7 100644 --- a/shoppingbasketapp/service/validator.go +++ b/shoppingbasketapp/service/cart/validator.go @@ -1,4 +1,4 @@ -package service +package cart import ( richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"