Merge pull request 'feat/shopping-basket' (#276) from feat/shopping-basket into develop

Reviewed-on: ebhomengo/niki#276
Reviewed-by: hossein <h.nazari1990@gmail.com>
This commit is contained in:
hossein 2026-04-15 05:54:13 +00:00
commit 39d85397d6
20 changed files with 969 additions and 5 deletions

View File

@ -0,0 +1 @@
package command

View File

@ -0,0 +1,52 @@
package command
import (
cfgloader "git.gocasts.ir/ebhomengo/niki/pkg/cfg_loader"
"git.gocasts.ir/ebhomengo/niki/pkg/path"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp"
"github.com/spf13/cobra"
"log"
"os"
"path/filepath"
)
var RootCmd = &cobra.Command{
Use: "shoppingbasket_service",
Short: "A CLI for shoppingbasket service",
Long: `shoppingbasket Service CLI is a tool to manage and run
the shoppingbasket service, including migrations and server startup.`,
}
func loadAppConfig() shoppingbasketapp.Config {
var cfg shoppingbasketapp.Config
projectRoot, err := path.PathProjectRoot()
if err != nil {
log.Fatalf("error finding project root: %v", err)
}
yamlPath := os.Getenv("CONFIG_PATH")
if yamlPath == "" {
defaultConfig := filepath.Join(projectRoot, "deploy", "shoppingbasket", "development", "config.yml")
if _, err := os.Stat(defaultConfig); err == nil {
yamlPath = defaultConfig
} else {
yamlPath = filepath.Join(projectRoot, "deploy", "shoppingbasket", "development", "config.local.yml")
}
}
options := cfgloader.Option{
Prefix: "SHOPPINGBASKET_",
Delimiter: ".",
Separator: "__",
YamlFilePath: yamlPath,
CallbackEnv: nil,
}
if err := cfgloader.Load(options, &cfg); err != nil {
log.Fatalf("Failed to load benefactor config: %v", err)
}
return cfg
}

View File

@ -0,0 +1,43 @@
package command
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp"
"github.com/labstack/gommon/log"
"github.com/spf13/cobra"
)
var ServeCmd = &cobra.Command{
Use: "serve",
Short: "Start shoppingbasket service",
Long: `This command starts the main shoppingbasket service.`,
Run: func(cmd *cobra.Command, args []string) {
},
}
func serve() {
var cfg = loadAppConfig()
logger.Init(cfg.Logger)
l := logger.L()
l.Info("Starting shoppingbasket service...")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app, err := shoppingbasketapp.Setup(ctx, cfg)
if err != nil {
l.Error("failed initialize shopping basket app", "error", err)
log.Fatalf(fmt.Sprintf("error starting shopping basket app: %v", err))
}
app.Start()
}
func init() {
RootCmd.AddCommand(ServeCmd)
}

View File

@ -0,0 +1,12 @@
package main
import (
"git.gocasts.ir/ebhomengo/niki/cmd/shoppingbasketapp/command"
"os"
)
func main() {
if err := command.RootCmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@ -0,0 +1,25 @@
redis:
host: "localhost"
port: 6379
password: ""
db: 0
repo:
kart_key_prefix: "shopping-basket-cart:"
ttl: 3600s
http_server:
host: "localhost"
port: 8080
shutdown_context_timeout: 10s
cors:
allow_origins:
- "*"
logger:
level: "debug" # Can be `debug`, `info`, `warn`, `error`
file_path: "logs/shoppingbasketapp/service.log"
use_local_time: true
file_max_size_in_mb: 10
file_max_age_in_days: 7

View File

@ -16,10 +16,10 @@ const (
)
type Config struct {
FilePath string
UseLocalTime bool
FileMaxSizeInMB int
FileMaxAgeInDays int
FilePath string `koanf:"file_path"`
UseLocalTime bool `koanf:"use_local_time"`
FileMaxSizeInMB int `koanf:"file_max_size_in_mb"`
FileMaxAgeInDays int `koanf:"file_max_age_in_days"`
}
var l *slog.Logger

View File

@ -3,11 +3,13 @@ package httpserver
import (
"context"
"fmt"
echomiddleware "github.com/gocasters/rankr/pkg/echo_middleware"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"strings"
"sync"
"time"
)
@ -19,7 +21,6 @@ type Config struct {
HideBanner bool `koanf:"hide_banner"`
HidePort bool `koanf:"hide_port"`
PublicPaths []string `koanf:"public_paths"`
// Optional Otel middleware can be injected from outside.
OtelMiddleware echo.MiddlewareFunc
}

126
shoppingbasketapp/app.go Normal file
View File

@ -0,0 +1,126 @@
package shoppingbasketapp
import (
"context"
"fmt"
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
"git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/delivery/http"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/service/cart"
"os"
"os/signal"
"sync"
"syscall"
)
type Application struct {
Repo repository.Repo
Service cart.Service
Handler http.Handler
Server http.Server
Config Config
}
func Setup(ctx context.Context, cfg Config) (Application, error) {
adapter := redis.New(cfg.Redis)
repo := repository.New(adapter.Client(), cfg.Repo)
validator := cart.NewValidate()
svc := cart.New(validator, repo)
handler := http.NewHandler(svc)
httpServer, err := httpserver.New(cfg.HTTPServer)
if err != nil {
logger.L().Error("failed to initialize http server", "error", err)
return Application{}, err
}
server := http.NewServer(handler, httpServer)
return Application{
Repo: repo,
Service: svc,
Handler: handler,
Server: server,
Config: cfg,
}, nil
}
func (app Application) Start() {
var wg sync.WaitGroup
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
startServers(app, &wg)
<-ctx.Done()
logger.L().Info("Shutdown signal received...")
shutdownTimeoutCtx, cancel := context.WithTimeout(context.Background(), app.Config.HTTPServer.ShutdownTimeout)
defer cancel()
if app.shutdownServers(shutdownTimeoutCtx) {
logger.L().Info("Servers shutdown gracefully")
} else {
logger.L().Warn("Shutdown timed out, exiting application")
return
}
wg.Wait()
logger.L().Info("shopping-basket-app stopped")
}
func startServers(app Application, wg *sync.WaitGroup) {
wg.Add(1)
go func() {
defer wg.Wait()
logger.L().Info(fmt.Sprintf("HTTP server starting on port: %d", app.Config.HTTPServer.Port))
if err := app.Server.Serve(); err != nil {
logger.L().Error(fmt.Sprintf("error listen and serve HTTP server on port %d", app.Config.HTTPServer.Port))
}
logger.L().Info(fmt.Sprintf("HTTP server stopped on port %d", app.Config.HTTPServer.Port))
}()
}
func (app Application) shutdownServers(ctx context.Context) bool {
logger.L().Info("Starting server shutdown process...")
shutdownDone := make(chan struct{})
go func() {
var shutdownWg sync.WaitGroup
shutdownWg.Add(1)
go app.shutdownHTTPServe(ctx, &shutdownWg)
shutdownWg.Wait()
close(shutdownDone)
logger.L().Info("All servers have been shut down successfully.")
}()
select {
case <-shutdownDone:
return true
case <-ctx.Done():
return false
}
}
func (app Application) shutdownHTTPServe(parentCtx context.Context, wg *sync.WaitGroup) {
logger.L().Info(fmt.Sprintf("Starting gracefully shutdown for http server on port %d", app.Config.HTTPServer.Port))
defer wg.Done()
httpShutdownCtx, httpCancel := context.WithTimeout(parentCtx, app.Config.HTTPServer.ShutdownTimeout)
defer httpCancel()
if err := app.Server.Stop(httpShutdownCtx); err != nil {
logger.L().Error(fmt.Sprintf("failed http server gracefully shutdown: %v", err))
}
logger.L().Info("Successfully http server gracefully shutdown")
}

View File

@ -0,0 +1,15 @@
package shoppingbasketapp
import (
"git.gocasts.ir/ebhomengo/niki/adapter/redis"
"git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
logger "git.gocasts.ir/ebhomengo/niki/pkg/logger"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository"
)
type Config struct {
Redis redis.Config `koanf:"redis" json:"redis"`
Repo repository.Config `koanf:"repo" json:"repo"`
HTTPServer httpserver.Config `koanf:"http_server" json:"http_server"`
Logger logger.Config `koanf:"logger" json:"logger"`
}

View File

@ -0,0 +1,117 @@
package http
import (
"git.gocasts.ir/ebhomengo/niki/pkg/claim"
httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg"
"git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/service/cart"
"git.gocasts.ir/ebhomengo/niki/types"
"github.com/labstack/echo/v4"
"net/http"
"strconv"
)
type Handler struct {
svc cart.Service
}
func NewHandler(svc cart.Service) Handler {
return Handler{svc: svc}
}
func (h Handler) addToBasket(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
var req cart.AddToCartRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid request body",
})
}
req.UserID = types.ID(claims.UserID)
if err := h.svc.AddToBasket(c.Request().Context(), req); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.NoContent(http.StatusNoContent)
}
func (h Handler) getCart(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
res, err := h.svc.GetCart(c.Request().Context(), types.ID(claims.UserID))
if err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.JSON(http.StatusOK, res)
}
func (h Handler) removeCart(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
if err := h.svc.ClearCart(c.Request().Context(), types.ID(claims.UserID)); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.NoContent(http.StatusNoContent)
}
func (h Handler) removeItem(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
p := c.Param("productID")
pID, err := strconv.Atoi(p)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid product id",
})
}
var req cart.RemoveFromCartRequest
req.UserID = types.ID(claims.UserID)
req.ProductID = types.ID(pID)
if err := h.svc.RemoveFromCart(c.Request().Context(), req); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.NoContent(http.StatusNoContent)
}
func (h Handler) updateQuantity(c echo.Context) error {
claims := claim.GetClaimsFromEchoContext(c)
p := c.Param("productID")
pID, err := strconv.Atoi(p)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid product id",
})
}
qStr := c.Param("quantity")
q, err := strconv.Atoi(qStr)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid quantity",
})
}
var req cart.UpdateQuantityRequest
req.UserID = types.ID(claims.UserID)
req.ProductID = types.ID(pID)
req.Quantity = q
if err := h.svc.UpdateQuantity(c.Request().Context(), req); err != nil {
msg, code := httpmsg.Error(err)
return c.JSON(code, msg)
}
return c.NoContent(http.StatusNoContent)
}

View File

@ -0,0 +1,12 @@
package http
import (
"github.com/labstack/echo/v4"
"net/http"
)
func (s Server) healthCheck(c echo.Context) error {
return c.JSON(http.StatusOK, echo.Map{
"message": "everything is good!",
})
}

View File

@ -0,0 +1,43 @@
package http
import (
"context"
"git.gocasts.ir/ebhomengo/niki/pkg/httpserver"
)
type Server struct {
handler Handler
HTTPServer *httpserver.Server
}
func NewServer(handler Handler, hS *httpserver.Server) Server {
return Server{handler: handler, HTTPServer: hS}
}
func (s Server) Serve() error {
s.registerRoutes()
if err := s.HTTPServer.Start(); err != nil {
return err
}
return nil
}
func (s Server) Stop(ctx context.Context) error {
return s.HTTPServer.Stop(ctx)
}
func (s Server) registerRoutes() {
router := s.HTTPServer.GetRouter()
router.GET("shoppingbasket/health-check", s.healthCheck)
r := router.Group("shoppingbasket/cart") // Authentication is required
r.GET("/", s.handler.getCart)
r.DELETE("/", s.handler.removeCart)
r.POST("/items", s.handler.addToBasket)
r.DELETE("/items/:productID", s.handler.removeItem)
r.PUT("/items/:productID/:quantity", s.handler.updateQuantity)
}

View File

@ -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
}

View File

@ -0,0 +1,21 @@
package cart
import (
"git.gocasts.ir/ebhomengo/niki/types"
)
type Item struct {
ProductID types.ID
Quantity int
Price types.Price
Name string
AddedAt int64
}
type Cart struct {
UserID types.ID
Items []Item
TotalPrice types.Price
ExpireAt int64
CreatedAt int64
}

View File

@ -0,0 +1,30 @@
package cart
import "git.gocasts.ir/ebhomengo/niki/types"
type AddToCartRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
Quantity int `json:"quantity"`
Price types.Price `json:"price"`
Name string `json:"name"`
}
type GetCartResponse struct {
UserID types.ID `json:"user_id"`
Items []Item `json:"items"`
TotalPrice types.Price `json:"total_price"`
CreatedAt int64 `json:"created_at"`
ExpireAt int64 `json:"expire_at"`
}
type RemoveFromCartRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
}
type UpdateQuantityRequest struct {
UserID types.ID `json:"user_id"`
ProductID types.ID `json:"product_id"`
Quantity int `json:"quantity"`
}

View File

@ -0,0 +1,103 @@
package cart
import (
"context"
"git.gocasts.ir/ebhomengo/niki/pkg/logger"
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
"git.gocasts.ir/ebhomengo/niki/types"
"time"
)
type Repository interface {
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
DeleteCart(ctx context.Context, userID types.ID) error
}
type Service struct {
validate Validate
repo Repository
}
func New(val Validate, repo Repository) Service {
return Service{validate: val, repo: repo}
}
func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error {
const op = "shoppingbasketapp.service.AddToBasket"
if err := s.validate.ValidateAddToCart(req); err != nil {
logger.L().Error("shoppingbasket-service-AddToBasket", "error", err)
return err
}
return s.repo.AddItem(ctx, req.UserID, Item{
ProductID: req.ProductID,
Quantity: req.Quantity,
Price: req.Price,
Name: req.Name,
AddedAt: time.Now().UnixNano(),
})
}
func (s Service) GetCart(ctx context.Context, userID types.ID) (GetCartResponse, error) {
const op = "shoppingbasketapp.service.GetCart"
if userID < 1 {
logger.L().Error("shoppingbasket-service-GetCart", "error", "user id must be greater than 1")
return GetCartResponse{}, richerror.New(op).WithKind(richerror.KindInvalid).WithMessage("invalid user id")
}
res, err := s.repo.GetCart(ctx, userID)
if err != nil {
logger.L().Error("shoppingbasket-service-GetCart", "error", err)
return GetCartResponse{}, richerror.New(op).WithErr(err)
}
return GetCartResponse{
UserID: res.UserID,
Items: res.Items,
TotalPrice: res.TotalPrice,
CreatedAt: res.CreatedAt,
ExpireAt: res.ExpireAt,
}, nil
}
func (s Service) RemoveFromCart(ctx context.Context, req RemoveFromCartRequest) error {
const op = "shoppingbaskerapp.service.RemoveFromCart"
if err := s.validate.ValidateRemoveFromCart(req); err != nil {
logger.L().Error("shoppingbasket-service-RemoveFromCart", "error", err)
return err
}
return s.repo.DeleteItem(ctx, req.UserID, req.ProductID)
}
func (s Service) UpdateQuantity(ctx context.Context, req UpdateQuantityRequest) error {
const op = "shoppingbaskerapp.service.UpdateQuantity"
if err := s.validate.ValidateUpdateQuantity(req); err != nil {
logger.L().Error("shoppingbasket-service-UpdateQuantity", "error", err)
return err
}
if req.Quantity == 0 {
return s.repo.DeleteItem(ctx, req.UserID, req.ProductID)
}
return s.repo.UpdateQuantity(ctx, req.UserID, req.ProductID, req.Quantity)
}
func (s Service) ClearCart(ctx context.Context, userID types.ID) error {
const op = "shoppingbaskerapp.service.ClearCart"
if userID < 1 {
logger.L().Error("shoppingbasket-service-ClearCart", "error", "user id must be greater than 1")
return richerror.New(op).WithKind(richerror.KindInvalid).
WithMessage("invalid user id")
}
return s.repo.DeleteCart(ctx, userID)
}

View File

@ -0,0 +1,91 @@
package cart
import (
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
const (
ErrValidationPositive = "must be positive"
ErrValidationInvalidInput = "invalid input"
)
type Validate struct{}
func NewValidate() Validate {
return Validate{}
}
func (v Validate) ValidateAddToCart(req AddToCartRequest) error {
const op = "shoppingbasketapp.service.AddToCart"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required),
validation.Field(&req.Price, validation.Required, validation.Min(int64(1)).Error(ErrValidationPositive)),
validation.Field(&req.Quantity, validation.Min(int(1)).Error(ErrValidationPositive)),
validation.Field(&req.Name, validation.Required)); err != nil {
fieldErr := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErr[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithMeta(fieldErr).WithErr(err).WithKind(richerror.KindInvalid)
}
return nil
}
func (v Validate) ValidateRemoveFromCart(req RemoveFromCartRequest) error {
const op = "shoppingbasketapp.service.ValidateRemoveFromCart"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required)); err != nil {
fieldErrs := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErrs[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithKind(richerror.KindInvalid).WithMeta(fieldErrs).WithErr(err)
}
return nil
}
func (v Validate) ValidateUpdateQuantity(req UpdateQuantityRequest) error {
const op = "shoppingbasketapp.service.ValidateUpdateQuantity"
if err := validation.ValidateStruct(&req,
validation.Field(&req.UserID, validation.Required),
validation.Field(&req.ProductID, validation.Required),
validation.Field(&req.Quantity, validation.Required, validation.Min(int(1)))); err != nil {
fieldErrs := make(map[string]interface{})
vErr, ok := err.(validation.Errors)
if ok {
for key, value := range vErr {
if value != nil {
fieldErrs[key] = value.Error()
}
}
}
return richerror.New(op).WithMessage(ErrValidationInvalidInput).
WithKind(richerror.KindInvalid).WithMeta(fieldErrs).WithErr(err)
}
return nil
}