From 73411137c8d65ad84d5bf86429e768eb8eb6f0f5 Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Fri, 10 Apr 2026 14:40:41 +0330 Subject: [PATCH 01/11] added shopping basket structure --- shoppingbasketapp/app.go | 1 + shoppingbasketapp/config.go | 1 + shoppingbasketapp/service/entity.go | 1 + shoppingbasketapp/service/param.go | 1 + shoppingbasketapp/service/service.go | 1 + 5 files changed, 5 insertions(+) create mode 100644 shoppingbasketapp/app.go create mode 100644 shoppingbasketapp/config.go create mode 100644 shoppingbasketapp/service/entity.go create mode 100644 shoppingbasketapp/service/param.go create mode 100644 shoppingbasketapp/service/service.go diff --git a/shoppingbasketapp/app.go b/shoppingbasketapp/app.go new file mode 100644 index 00000000..8505883b --- /dev/null +++ b/shoppingbasketapp/app.go @@ -0,0 +1 @@ +package shoppingbasketapp diff --git a/shoppingbasketapp/config.go b/shoppingbasketapp/config.go new file mode 100644 index 00000000..8505883b --- /dev/null +++ b/shoppingbasketapp/config.go @@ -0,0 +1 @@ +package shoppingbasketapp diff --git a/shoppingbasketapp/service/entity.go b/shoppingbasketapp/service/entity.go new file mode 100644 index 00000000..6d43c336 --- /dev/null +++ b/shoppingbasketapp/service/entity.go @@ -0,0 +1 @@ +package service diff --git a/shoppingbasketapp/service/param.go b/shoppingbasketapp/service/param.go new file mode 100644 index 00000000..6d43c336 --- /dev/null +++ b/shoppingbasketapp/service/param.go @@ -0,0 +1 @@ +package service diff --git a/shoppingbasketapp/service/service.go b/shoppingbasketapp/service/service.go new file mode 100644 index 00000000..6d43c336 --- /dev/null +++ b/shoppingbasketapp/service/service.go @@ -0,0 +1 @@ +package service From 3d5e4e473b1e34adfc9b865849eb0fb940d2bbcf Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Fri, 10 Apr 2026 18:52:30 +0330 Subject: [PATCH 02/11] implemented service layer --- shoppingbasketapp/service/entity.go | 20 ++++++ shoppingbasketapp/service/param.go | 29 ++++++++ shoppingbasketapp/service/service.go | 95 ++++++++++++++++++++++++++ shoppingbasketapp/service/validator.go | 91 ++++++++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 shoppingbasketapp/service/validator.go diff --git a/shoppingbasketapp/service/entity.go b/shoppingbasketapp/service/entity.go index 6d43c336..6f10c37f 100644 --- a/shoppingbasketapp/service/entity.go +++ b/shoppingbasketapp/service/entity.go @@ -1 +1,21 @@ package service + +import ( + "git.gocasts.ir/ebhomengo/niki/types" +) + +type CartItem struct { + ProductID types.ID + Quantity int + Price types.Price + Name string + AddedAt int64 +} + +type Cart struct { + UserID types.ID + Items []CartItem + TotalPrice types.Price + ExpireAt int64 + CreatedAt int64 +} diff --git a/shoppingbasketapp/service/param.go b/shoppingbasketapp/service/param.go index 6d43c336..264c053e 100644 --- a/shoppingbasketapp/service/param.go +++ b/shoppingbasketapp/service/param.go @@ -1 +1,30 @@ package service + +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 []CartItem `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"` +} diff --git a/shoppingbasketapp/service/service.go b/shoppingbasketapp/service/service.go index 6d43c336..59d5a00b 100644 --- a/shoppingbasketapp/service/service.go +++ b/shoppingbasketapp/service/service.go @@ -1 +1,96 @@ package service + +import ( + "context" + 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, cart CartItem) 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 { + return err + } + + return s.repo.AddItem(ctx, req.UserID, CartItem{ + 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 { + return GetCartResponse{}, richerror.New(op).WithKind(richerror.KindInvalid).WithMessage("invalid user id") + } + + res, err := s.repo.GetCart(ctx, userID) + if err != nil { + 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 { + 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 { + 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 { + return richerror.New(op).WithKind(richerror.KindInvalid). + WithMessage("invalid user id") + } + + return s.repo.DeleteCart(ctx, userID) +} diff --git a/shoppingbasketapp/service/validator.go b/shoppingbasketapp/service/validator.go new file mode 100644 index 00000000..8ab07773 --- /dev/null +++ b/shoppingbasketapp/service/validator.go @@ -0,0 +1,91 @@ +package service + +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 +} From c1ed70cf669c8c45de4f365af2065a31cbcd5e3d Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Sun, 12 Apr 2026 21:32:23 +0330 Subject: [PATCH 03/11] 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" From 4ad9199a651b5ff91757945afde566cdfaa9e7fa Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Sun, 12 Apr 2026 23:34:27 +0330 Subject: [PATCH 04/11] implemented echo server instance in pkg directory --- pkg/httpserver/constant.go | 7 ++++ pkg/httpserver/server.go | 83 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 pkg/httpserver/constant.go create mode 100644 pkg/httpserver/server.go diff --git a/pkg/httpserver/constant.go b/pkg/httpserver/constant.go new file mode 100644 index 00000000..30be3c74 --- /dev/null +++ b/pkg/httpserver/constant.go @@ -0,0 +1,7 @@ +package httpserver + +import "time" + +const ( + DefaultShutdownTimeout = 10 * time.Second +) diff --git a/pkg/httpserver/server.go b/pkg/httpserver/server.go new file mode 100644 index 00000000..05e9867f --- /dev/null +++ b/pkg/httpserver/server.go @@ -0,0 +1,83 @@ +package httpserver + +import ( + "context" + "fmt" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "time" +) + +type Config struct { + Host string `koanf:"host"` + Port int `koanf:"port"` + CORS CORS `koanf:"cors"` + ShutdownTimeout time.Duration `koanf:"shutdown_context_timeout"` + HideBanner bool `koanf:"hide_banner"` + HidePort bool `koanf:"hide_port"` + + // Optional Otel middleware can be injected from outside. + OtelMiddleware echo.MiddlewareFunc +} + +type CORS struct { + AllowOrigins []string `koanf:"allow_origins"` +} + +type Server struct { + router *echo.Echo + config *Config +} + +func New(cfg Config) (*Server, error) { + if cfg.Port < 1 || cfg.Port > 65535 { + return nil, fmt.Errorf("invalid port: %d", cfg.Port) + } + + if cfg.ShutdownTimeout <= 0 { + cfg.ShutdownTimeout = DefaultShutdownTimeout + } + + e := echo.New() + + if cfg.OtelMiddleware != nil { + e.Use(cfg.OtelMiddleware) + } + + e.Use(middleware.RequestID()) + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use( + middleware.CORSWithConfig( + middleware.CORSConfig{ + AllowOrigins: cfg.CORS.AllowOrigins, + }, + ), + ) + + return &Server{ + router: e, + config: &cfg, + }, nil +} + +func (s *Server) GetRouter() *echo.Echo { + return s.router +} + +func (s *Server) GetConfig() *Config { + return s.config +} + +func (s *Server) Start() error { + addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port) + + s.router.HideBanner = s.config.HideBanner + s.router.HidePort = s.config.HidePort + + return s.router.Start(addr) +} + +func (s *Server) Stop(ctx context.Context) error { + return s.router.Shutdown(ctx) +} From 8dd13ce6af7a4de1df65d74a765557ab0b3d6840 Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Sun, 12 Apr 2026 23:34:52 +0330 Subject: [PATCH 05/11] added delivery layer --- shoppingbasketapp/config.go | 6 +- shoppingbasketapp/delivery/http/handler.go | 117 ++++++++++++++++++ .../delivery/http/health_check.go | 12 ++ shoppingbasketapp/delivery/http/server.go | 43 +++++++ 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 shoppingbasketapp/delivery/http/handler.go create mode 100644 shoppingbasketapp/delivery/http/health_check.go create mode 100644 shoppingbasketapp/delivery/http/server.go diff --git a/shoppingbasketapp/config.go b/shoppingbasketapp/config.go index 991d4f84..c87b97df 100644 --- a/shoppingbasketapp/config.go +++ b/shoppingbasketapp/config.go @@ -2,10 +2,12 @@ package shoppingbasketapp import ( "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" ) type Config struct { - Redis redis.Config `koanf:"redis" json:"redis"` - Repo repository.Repo `koanf:"repo" json:"repo"` + Redis redis.Config `koanf:"redis" json:"redis"` + Repo repository.Repo `koanf:"repo" json:"repo"` + HTTPServer httpserver.Config `koanf:"http_server" json:"http_server"` } diff --git a/shoppingbasketapp/delivery/http/handler.go b/shoppingbasketapp/delivery/http/handler.go new file mode 100644 index 00000000..f593c076 --- /dev/null +++ b/shoppingbasketapp/delivery/http/handler.go @@ -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) +} diff --git a/shoppingbasketapp/delivery/http/health_check.go b/shoppingbasketapp/delivery/http/health_check.go new file mode 100644 index 00000000..aa0a0254 --- /dev/null +++ b/shoppingbasketapp/delivery/http/health_check.go @@ -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!", + }) +} diff --git a/shoppingbasketapp/delivery/http/server.go b/shoppingbasketapp/delivery/http/server.go new file mode 100644 index 00000000..cfe5fe8c --- /dev/null +++ b/shoppingbasketapp/delivery/http/server.go @@ -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) +} From f4bd43a60f1ceb0d486b68bc9b4e516eac973aa5 Mon Sep 17 00:00:00 2001 From: Sahar Mokarrami Date: Mon, 13 Apr 2026 10:32:58 +0330 Subject: [PATCH 06/11] feat(order): add order domain structure --- cmd/purchaseapp/main.go | 2 +- domain/order/entity/order.go | 49 +++++++++++++++ domain/order/entity/order_item.go | 16 +++++ domain/order/entity/shipping.go | 10 ++++ .../2026010411120_create_orders_table.sql | 8 +-- ...0260104_11121_create_order_items_table.sql | 0 .../20261104_11122_create_shippings_table.sql | 16 +++++ domain/order/repository/mysql/db.go | 11 ++++ .../order}/repository/mysql/order.go | 51 +++++++++++----- .../order/service/order.go | 19 +++--- domain/order/service/shipping.go | 16 +++++ purchaseapp/app.go | 2 +- purchaseapp/delivery/http/order/handler.go | 8 +-- .../{service => delivery/http}/order/param.go | 4 +- purchaseapp/delivery/http/order/route.go | 1 - purchaseapp/delivery/http/server.go | 10 ++-- purchaseapp/entity/order.go | 59 ------------------- purchaseapp/repository/mysql/db.go | 1 - purchaseapp/service/order/validator.go | 1 - 19 files changed, 182 insertions(+), 102 deletions(-) create mode 100644 domain/order/entity/order.go create mode 100644 domain/order/entity/order_item.go create mode 100644 domain/order/entity/shipping.go rename {purchaseapp => domain/order}/repository/migrations/2026010411120_create_orders_table.sql (84%) rename {purchaseapp => domain/order}/repository/migrations/20260104_11121_create_order_items_table.sql (100%) create mode 100644 domain/order/repository/migrations/20261104_11122_create_shippings_table.sql create mode 100644 domain/order/repository/mysql/db.go rename {purchaseapp => domain/order}/repository/mysql/order.go (62%) rename purchaseapp/service/order/service.go => domain/order/service/order.go (52%) create mode 100644 domain/order/service/shipping.go rename purchaseapp/{service => delivery/http}/order/param.go (82%) delete mode 100644 purchaseapp/entity/order.go delete mode 100644 purchaseapp/repository/mysql/db.go delete mode 100644 purchaseapp/service/order/validator.go diff --git a/cmd/purchaseapp/main.go b/cmd/purchaseapp/main.go index fdd06b0b..ad0283df 100644 --- a/cmd/purchaseapp/main.go +++ b/cmd/purchaseapp/main.go @@ -3,8 +3,8 @@ package main import ( "flag" "fmt" + purchaseMysql "git.gocasts.ir/ebhomengo/niki/domain/purchase/repository/mysql" "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http" - purchaseMysql "git.gocasts.ir/ebhomengo/niki/purchaseapp/repository/mysql" "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order" "git.gocasts.ir/ebhomengo/niki/repository/migrator" "git.gocasts.ir/ebhomengo/niki/repository/mysql" diff --git a/domain/order/entity/order.go b/domain/order/entity/order.go new file mode 100644 index 00000000..89f5499e --- /dev/null +++ b/domain/order/entity/order.go @@ -0,0 +1,49 @@ +package entity + +import ( + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) + +type Order struct { + ID types.ID + UserID types.ID + TotalAmount types.Price + TotalDiscount types.Price + ShippingID types.ID + PaymentMethod PaymentMethod + ProcessStatus ProcessStatus + PaymentStatus PaymentStatus + AddressID types.ID + CreatedAt time.Time + UpdatedAt time.Time +} + +type PaymentMethod string + +const ( + Online PaymentMethod = "online" + Wallet = "wallet" + Cart = "cart" +) + +type ProcessStatus string + +const ( + WaitingToPay ProcessStatus = "waiting-to-pay" + Processing = "processing" + Accepted = "accepted" + Preparing = "preparing" + Prepared = "prepared" + GivenToPost = "given-to-post" + Delivered = "delivered" + Cancelled = "cancelled" + SystemCancellation = "system-cancellation" +) + +type PaymentStatus string + +const ( + Paid PaymentStatus = "paid" + UnPaid = "unpaid" +) diff --git a/domain/order/entity/order_item.go b/domain/order/entity/order_item.go new file mode 100644 index 00000000..0a9db823 --- /dev/null +++ b/domain/order/entity/order_item.go @@ -0,0 +1,16 @@ +package entity + +import ( + "git.gocasts.ir/ebhomengo/niki/types" + "time" +) + +type OrderItem struct { + ID types.ID + ProductID types.ID + Price types.Price + Quantity types.Count + PriceWithDiscount types.Price + OrderID types.ID + CreatedAt time.Time +} diff --git a/domain/order/entity/shipping.go b/domain/order/entity/shipping.go new file mode 100644 index 00000000..53d518ca --- /dev/null +++ b/domain/order/entity/shipping.go @@ -0,0 +1,10 @@ +package entity + +import "git.gocasts.ir/ebhomengo/niki/types" + +type Shipping struct { + ID types.ID + Name string + Price types.Price + IsActive bool +} diff --git a/purchaseapp/repository/migrations/2026010411120_create_orders_table.sql b/domain/order/repository/migrations/2026010411120_create_orders_table.sql similarity index 84% rename from purchaseapp/repository/migrations/2026010411120_create_orders_table.sql rename to domain/order/repository/migrations/2026010411120_create_orders_table.sql index 2fe04c92..55ff9490 100644 --- a/purchaseapp/repository/migrations/2026010411120_create_orders_table.sql +++ b/domain/order/repository/migrations/2026010411120_create_orders_table.sql @@ -3,11 +3,11 @@ -- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text CREATE TABLE `orders` ( `id` INT PRIMARY KEY AUTO_INCREMENT, - `user_id` INT NOT NULL, - `address` TEXT, + `user_id` INT, + `address_id` INT, `shipping_id` INT NOT NULL, `payment_method` ENUM('online', 'wallet', 'cart') DEFAULT 'online', - `payment_status` ENUM('unpaid', 'paid', 'cancelled') DEFAULT 'unpaid', + `payment_status` ENUM('unpaid', 'paid') DEFAULT 'unpaid', `process_status` ENUM('waiting-to-pay', 'processing', 'accepted', 'preparing', 'prepared', 'given-to-post', 'delivered', 'cancelled') DEFAULT 'waiting-to-pay', `total_amount` INT NOT NULL, `total_discount` INT NULL, @@ -15,7 +15,7 @@ CREATE TABLE `orders` ( `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) --- FOREIGN KEY (`shipping_id`) REFERENCES `shippings`(`id`) + FOREIGN KEY (`shipping_id`) REFERENCES `shippings`(`id`) ); diff --git a/purchaseapp/repository/migrations/20260104_11121_create_order_items_table.sql b/domain/order/repository/migrations/20260104_11121_create_order_items_table.sql similarity index 100% rename from purchaseapp/repository/migrations/20260104_11121_create_order_items_table.sql rename to domain/order/repository/migrations/20260104_11121_create_order_items_table.sql diff --git a/domain/order/repository/migrations/20261104_11122_create_shippings_table.sql b/domain/order/repository/migrations/20261104_11122_create_shippings_table.sql new file mode 100644 index 00000000..4a1375c0 --- /dev/null +++ b/domain/order/repository/migrations/20261104_11122_create_shippings_table.sql @@ -0,0 +1,16 @@ +-- +migrate Up +-- please read this article to understand why we use VARCHAR(191) +-- https://www.grouparoo.com/blog/varchar-191#why-varchar-and-not-text +CREATE TABLE `orders` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `name` VARCHAR (191), + `price` INT NOT NULL , + `is_active` INT NOT NULL DEFAULT 1, + + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + +); + +-- +migrate Down +DROP TABLE `orders`; diff --git a/domain/order/repository/mysql/db.go b/domain/order/repository/mysql/db.go new file mode 100644 index 00000000..0082acb0 --- /dev/null +++ b/domain/order/repository/mysql/db.go @@ -0,0 +1,11 @@ +package mysql + +import "git.gocasts.ir/ebhomengo/niki/repository/mysql" + +type DB struct { + conn *mysql.DB +} + +func New(db *mysql.DB) *DB { + return &DB{conn: db} +} diff --git a/purchaseapp/repository/mysql/order.go b/domain/order/repository/mysql/order.go similarity index 62% rename from purchaseapp/repository/mysql/order.go rename to domain/order/repository/mysql/order.go index 279aa351..748b6be0 100644 --- a/purchaseapp/repository/mysql/order.go +++ b/domain/order/repository/mysql/order.go @@ -1,23 +1,14 @@ package mysql import ( + entity "git.gocasts.ir/ebhomengo/niki/domain/order/entity" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" - "git.gocasts.ir/ebhomengo/niki/purchaseapp/entity" - "git.gocasts.ir/ebhomengo/niki/repository/mysql" "git.gocasts.ir/ebhomengo/niki/types" ) -type DB struct { - conn *mysql.DB -} - -func New(db *mysql.DB) *DB { - return &DB{conn: db} -} - func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) { - const Op = "repository.mysql.order.createorder" + const Op = "domain.repository.mysql.order.create-order" tx, err := d.conn.Conn().Begin() if err != nil { @@ -26,10 +17,10 @@ func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (typ defer tx.Rollback() - query := "insert into orders(user_id, address, shipping_id," + + query := "insert into orders(user_id, address_id, shipping_id," + " payment_method, payment_status, process_status," + " total_amount, total_discount) values (?, ?, ?, ?, ?, ?, ?, ?);" - res, oErr := tx.Exec(query, order.UserID, order.Address, order.ShippingID, + res, oErr := tx.Exec(query, order.UserID, order.AddressID, order.ShippingID, order.PaymentMethod, order.PaymentStatus, order.ProcessStatus, order.TotalAmount, order.TotalDiscount) @@ -57,7 +48,7 @@ func (d *DB) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (typ } func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) { - const Op = "repository.mysql.order.update-order-process-status" + const Op = "domain.repository.mysql.order.update-order-process-status" _, err := d.conn.Conn().Exec("update orders set process_status=? where id=?;", status, orderID) if err != nil { @@ -67,3 +58,35 @@ func (d *DB) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, er return true, nil } + +func (d *DB) GetShipping() ([]entity.Shipping, error) { + const Op = "domain.repository.mysql.order.get-shipping" + rows, err := d.conn.Conn().Query("select * from shippings where is_active=1") + + if err != nil { + return []entity.Shipping{}, richerror.New(Op).WithErr(err) + } + + defer rows.Close() + + var shippings []entity.Shipping + + for rows.Next() { + var s entity.Shipping + + err := rows.Scan( + &s.ID, + &s.Name, + &s.Price, + &s.IsActive, + ) + if err != nil { + return nil, richerror.New(Op).WithErr(err) + } + + shippings = append(shippings, s) + } + + return shippings, nil + +} diff --git a/purchaseapp/service/order/service.go b/domain/order/service/order.go similarity index 52% rename from purchaseapp/service/order/service.go rename to domain/order/service/order.go index 21690140..40255d49 100644 --- a/purchaseapp/service/order/service.go +++ b/domain/order/service/order.go @@ -1,9 +1,9 @@ -package order +package service import ( + entity "git.gocasts.ir/ebhomengo/niki/domain/order/entity" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" - "git.gocasts.ir/ebhomengo/niki/purchaseapp/entity" - "git.gocasts.ir/ebhomengo/niki/types" + types "git.gocasts.ir/ebhomengo/niki/types" ) type Service struct { @@ -13,26 +13,27 @@ type Service struct { type Repo interface { CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) + GetShipping() ([]entity.Shipping, error) } func New(orderRepo Repo) Service { return Service{repo: orderRepo} } -func (s Service) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (CreateOrderResponse, error) { - const Op = "purchaseapp.service.CreateOrder" +func (s *Service) CreateOrder(order entity.Order, orderItems []entity.OrderItem) (types.ID, error) { + const Op = "domain.order.service.order.CreateOrder" orderID, err := s.repo.CreateOrder(order, orderItems) if err != nil { - return CreateOrderResponse{}, richerror.New(Op).WithErr(err) + return 0, richerror.New(Op).WithErr(err) } - return CreateOrderResponse{OrderID: orderID}, nil + return orderID, nil } -func (s Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) { +func (s *Service) UpdateOrderProcessStatus(orderID types.ID, status string) (bool, error) { - const Op = "purchaseapp.service.UpdateOrderProcessStatus" + const Op = "domain.order.service.order.UpdateOrderProcessStatus" _, err := s.repo.UpdateOrderProcessStatus(orderID, status) if err != nil { return false, richerror.New(Op).WithErr(err) diff --git a/domain/order/service/shipping.go b/domain/order/service/shipping.go new file mode 100644 index 00000000..3d1f6dae --- /dev/null +++ b/domain/order/service/shipping.go @@ -0,0 +1,16 @@ +package service + +import ( + "git.gocasts.ir/ebhomengo/niki/domain/order/entity" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (s *Service) GetShipping() ([]entity.Shipping, error) { + const Op = "domain.order.service.shipping.get-shipping" + shippings, err := s.repo.GetShipping() + if err != nil { + return []entity.Shipping{}, richerror.New(Op) + } + + return shippings, nil +} diff --git a/purchaseapp/app.go b/purchaseapp/app.go index 7f62b636..a846535b 100644 --- a/purchaseapp/app.go +++ b/purchaseapp/app.go @@ -3,9 +3,9 @@ package purchaseapp import ( "context" "fmt" + purchaseMysql "git.gocasts.ir/ebhomengo/niki/domain/purchase/repository/mysql" purchaseHTTP "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http" purchaseHandler "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order" - purchaseMysql "git.gocasts.ir/ebhomengo/niki/purchaseapp/repository/mysql" purchaseService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order" "git.gocasts.ir/ebhomengo/niki/repository/mysql" ) diff --git a/purchaseapp/delivery/http/order/handler.go b/purchaseapp/delivery/http/order/handler.go index 9ae96087..e9ba97f1 100644 --- a/purchaseapp/delivery/http/order/handler.go +++ b/purchaseapp/delivery/http/order/handler.go @@ -1,9 +1,9 @@ package order import ( + entity "git.gocasts.ir/ebhomengo/niki/domain/order/entity" + order "git.gocasts.ir/ebhomengo/niki/domain/order/service" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" - "git.gocasts.ir/ebhomengo/niki/purchaseapp/entity" - "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order" "github.com/labstack/echo/v4" "net/http" "time" @@ -18,7 +18,7 @@ func New(orderSvc order.Service) *Handler { } func (h *Handler) CreateOrderHandler(c echo.Context) error { - var req order.CreateOrderRequest + var req CreateOrderRequest if err := c.Bind(&req); err != nil { msg, code := getErrorDataFromRichError(err) return echo.NewHTTPError(code, msg) @@ -34,7 +34,7 @@ func (h *Handler) CreateOrderHandler(c echo.Context) error { PaymentMethod: req.PaymentMethod, ProcessStatus: entity.WaitingToPay, PaymentStatus: entity.UnPaid, - Address: req.Address, + AddressID: req.AddressID, CreatedAt: time.Now(), UpdatedAt: time.Now(), } diff --git a/purchaseapp/service/order/param.go b/purchaseapp/delivery/http/order/param.go similarity index 82% rename from purchaseapp/service/order/param.go rename to purchaseapp/delivery/http/order/param.go index d4a589f9..dac464ba 100644 --- a/purchaseapp/service/order/param.go +++ b/purchaseapp/delivery/http/order/param.go @@ -1,13 +1,13 @@ package order import ( - "git.gocasts.ir/ebhomengo/niki/purchaseapp/entity" + "git.gocasts.ir/ebhomengo/niki/domain/order/entity" "git.gocasts.ir/ebhomengo/niki/types" ) type CreateOrderRequest struct { UserID types.ID `json:"user_id"` - Address string `json:"address"` + AddressID types.ID `json:"address_id"` ShippingID types.ID `json:"shipping_id"` PaymentMethod entity.PaymentMethod `json:"payment_method"` TotalAmount types.Price `json:"total_amount"` diff --git a/purchaseapp/delivery/http/order/route.go b/purchaseapp/delivery/http/order/route.go index 3db0dd1f..62b20d3c 100644 --- a/purchaseapp/delivery/http/order/route.go +++ b/purchaseapp/delivery/http/order/route.go @@ -4,5 +4,4 @@ import "github.com/labstack/echo/v4" func (h Handler) SetRoutes(e *echo.Echo) { - e.POST("/order/create", h.CreateOrderHandler) } diff --git a/purchaseapp/delivery/http/server.go b/purchaseapp/delivery/http/server.go index 1da710cb..0852e026 100644 --- a/purchaseapp/delivery/http/server.go +++ b/purchaseapp/delivery/http/server.go @@ -1,19 +1,19 @@ package http import ( - "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order" - orderService "git.gocasts.ir/ebhomengo/niki/purchaseapp/service/order" + order "git.gocasts.ir/ebhomengo/niki/domain/order/service" + orderHandler "git.gocasts.ir/ebhomengo/niki/purchaseapp/delivery/http/order" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) type Server struct { - OrderHandler *order.Handler + OrderHandler *orderHandler.Handler } -func New(orderSvc orderService.Service) *Server { +func New(orderSvc order.Service) *Server { return &Server{ - OrderHandler: order.New(orderSvc), + OrderHandler: orderHandler.New(orderSvc), } } diff --git a/purchaseapp/entity/order.go b/purchaseapp/entity/order.go deleted file mode 100644 index 0e235f66..00000000 --- a/purchaseapp/entity/order.go +++ /dev/null @@ -1,59 +0,0 @@ -package entity - -import ( - "git.gocasts.ir/ebhomengo/niki/types" - "time" -) - -type Order struct { - ID types.ID - UserID types.ID - TotalAmount types.Price - TotalDiscount types.Price - ShippingID types.ID - PaymentMethod PaymentMethod - ProcessStatus ProcessStatus - PaymentStatus PaymentStatus - Address string - CreatedAt time.Time - UpdatedAt time.Time -} - -type OrderItem struct { - ID types.ID - ProductID types.ID - Price types.Price - Quantity types.Count - PriceWithDiscount types.Price - OrderID types.ID - CreatedAt time.Time -} - -type PaymentMethod string - -const ( - Online PaymentMethod = "online" - Wallet = "wallet" - Cart = "cart" -) - -type ProcessStatus string - -const ( - WaitingToPay ProcessStatus = "waiting-to-pay" - processing = "processing" - accepted = "accepted" - preparing = "preparing" - prepared = "prepared" - givenToPost = "given-to-post" - delivered = "delivered" - cancelled = "cancelled" -) - -type PaymentStatus string - -const ( - Paid PaymentStatus = "paid" - UnPaid = "unpaid" - Cancelled = "cancelled" -) diff --git a/purchaseapp/repository/mysql/db.go b/purchaseapp/repository/mysql/db.go deleted file mode 100644 index b0843023..00000000 --- a/purchaseapp/repository/mysql/db.go +++ /dev/null @@ -1 +0,0 @@ -package mysql diff --git a/purchaseapp/service/order/validator.go b/purchaseapp/service/order/validator.go deleted file mode 100644 index 175f0c10..00000000 --- a/purchaseapp/service/order/validator.go +++ /dev/null @@ -1 +0,0 @@ -package order From 50d7c2a2dc73b19f99aaaa6c01a766d3a3282e8e Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Mon, 13 Apr 2026 23:51:03 +0330 Subject: [PATCH 07/11] added app configuration and bootstrap setup --- shoppingbasketapp/app.go | 131 ++++++++++++++++++++++ shoppingbasketapp/config.go | 4 +- shoppingbasketapp/service/cart/service.go | 12 +- 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/shoppingbasketapp/app.go b/shoppingbasketapp/app.go index 8505883b..0f0258e8 100644 --- a/shoppingbasketapp/app.go +++ b/shoppingbasketapp/app.go @@ -1 +1,132 @@ package shoppingbasketapp + +import ( + "context" + "fmt" + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + logger "git.gocasts.ir/ebhomengo/niki/logger" + "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/delivery/http" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/service/cart" + "log/slog" + "os" + "os/signal" + "sync" + "syscall" +) + +type Application struct { + Repo repository.Repo + Service cart.Service + Handler http.Handler + Server http.Server + Config Config + Logger *slog.Logger +} + +func (app Application) Setup(cfg Config) (Application, error) { + l := logger.New(cfg.Logger, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + + adapter := redis.New(cfg.Redis) + repo := repository.New(adapter.Client(), cfg.Repo) + + validator := cart.NewValidate() + svc := cart.New(validator, l, repo) + + handler := http.NewHandler(svc) + + httpServer, err := httpserver.New(cfg.HTTPServer) + if err != nil { + 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, + Logger: l, + }, 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() + + app.Logger.Info("Shutdown signal received...") + + shutdownTimeoutCtx, cancel := context.WithTimeout(context.Background(), app.Config.HTTPServer.ShutdownTimeout) + defer cancel() + + if app.shutdownServers(shutdownTimeoutCtx) { + app.Logger.Info("Servers shutdown gracefully") + } else { + app.Logger.Warn("Shutdown timed out, exiting application") + return + } + + wg.Wait() + app.Logger.Info("shopping-basket-app stopped") +} + +func startServers(app Application, wg *sync.WaitGroup) { + wg.Add(1) + go func() { + defer wg.Wait() + app.Logger.Info(fmt.Sprintf("HTTP server starting on port: %d", app.Config.HTTPServer.Port)) + if err := app.Server.Serve(); err != nil { + app.Logger.Error(fmt.Sprintf("error listen and serve HTTP server on port %d", app.Config.HTTPServer.Port)) + } + + app.Logger.Info(fmt.Sprintf("HTTP server stopped on port %d", app.Config.HTTPServer.Port)) + }() +} + +func (app Application) shutdownServers(ctx context.Context) bool { + app.Logger.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) + app.Logger.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) { + app.Logger.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 { + app.Logger.Error(fmt.Sprintf("failed http server gracefully shutdown: %v", err)) + } + + app.Logger.Info("Successfully http server gracefully shutdown") +} diff --git a/shoppingbasketapp/config.go b/shoppingbasketapp/config.go index c87b97df..39a0e4af 100644 --- a/shoppingbasketapp/config.go +++ b/shoppingbasketapp/config.go @@ -2,12 +2,14 @@ package shoppingbasketapp import ( "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/logger" "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" ) type Config struct { Redis redis.Config `koanf:"redis" json:"redis"` - Repo repository.Repo `koanf:"repo" json:"repo"` + Repo repository.Config `koanf:"repo" json:"repo"` HTTPServer httpserver.Config `koanf:"http_server" json:"http_server"` + Logger logger.Config `koanf:"logger" json:"logger"` } diff --git a/shoppingbasketapp/service/cart/service.go b/shoppingbasketapp/service/cart/service.go index df860b0b..d84a3f73 100644 --- a/shoppingbasketapp/service/cart/service.go +++ b/shoppingbasketapp/service/cart/service.go @@ -4,6 +4,7 @@ import ( "context" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" "git.gocasts.ir/ebhomengo/niki/types" + "log/slog" "time" ) @@ -17,17 +18,19 @@ type Repository interface { type Service struct { validate Validate + logger *slog.Logger repo Repository } -func New(val Validate, repo Repository) Service { - return Service{validate: val, repo: repo} +func New(val Validate, logger *slog.Logger, repo Repository) Service { + return Service{validate: val, logger: logger, 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 { + s.logger.Error("shoppingbasket-service-AddToBasket", "error", err) return err } @@ -43,11 +46,13 @@ func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error { func (s Service) GetCart(ctx context.Context, userID types.ID) (GetCartResponse, error) { const op = "shoppingbasketapp.service.GetCart" if userID < 1 { + s.logger.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 { + s.logger.Error("shoppingbasket-service-GetCart", "error", err) return GetCartResponse{}, richerror.New(op).WithErr(err) } @@ -64,6 +69,7 @@ func (s Service) RemoveFromCart(ctx context.Context, req RemoveFromCartRequest) const op = "shoppingbaskerapp.service.RemoveFromCart" if err := s.validate.ValidateRemoveFromCart(req); err != nil { + s.logger.Error("shoppingbasket-service-RemoveFromCart", "error", err) return err } @@ -74,6 +80,7 @@ func (s Service) UpdateQuantity(ctx context.Context, req UpdateQuantityRequest) const op = "shoppingbaskerapp.service.UpdateQuantity" if err := s.validate.ValidateUpdateQuantity(req); err != nil { + s.logger.Error("shoppingbasket-service-UpdateQuantity", "error", err) return err } @@ -88,6 +95,7 @@ func (s Service) ClearCart(ctx context.Context, userID types.ID) error { const op = "shoppingbaskerapp.service.ClearCart" if userID < 1 { + s.logger.Error("shoppingbasket-service-ClearCart", "error", "user id must be greater than 1") return richerror.New(op).WithKind(richerror.KindInvalid). WithMessage("invalid user id") } From 2037e58a1d4282d3cf7b206745ea3a3507408c92 Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Tue, 14 Apr 2026 16:29:03 +0330 Subject: [PATCH 08/11] added cmd and deploy layer --- cmd/shoppingbasketapp/command/migrate.go | 1 + cmd/shoppingbasketapp/command/root.go | 1 + cmd/shoppingbasketapp/command/serve.go | 1 + cmd/shoppingbasketapp/main.go | 44 +++++++++++++++++++ .../shoppingbasket/development/.env.example | 0 deploy/shoppingbasket/development/Dockerfile | 0 deploy/shoppingbasket/development/config.yml | 20 +++++++++ .../development/docker-compose.yml | 0 logger/logger.go | 8 ++-- shoppingbasketapp/app.go | 2 +- 10 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 cmd/shoppingbasketapp/command/migrate.go create mode 100644 cmd/shoppingbasketapp/command/root.go create mode 100644 cmd/shoppingbasketapp/command/serve.go create mode 100644 cmd/shoppingbasketapp/main.go create mode 100644 deploy/shoppingbasket/development/.env.example create mode 100644 deploy/shoppingbasket/development/Dockerfile create mode 100644 deploy/shoppingbasket/development/config.yml create mode 100644 deploy/shoppingbasket/development/docker-compose.yml diff --git a/cmd/shoppingbasketapp/command/migrate.go b/cmd/shoppingbasketapp/command/migrate.go new file mode 100644 index 00000000..d47dcf0d --- /dev/null +++ b/cmd/shoppingbasketapp/command/migrate.go @@ -0,0 +1 @@ +package command diff --git a/cmd/shoppingbasketapp/command/root.go b/cmd/shoppingbasketapp/command/root.go new file mode 100644 index 00000000..d47dcf0d --- /dev/null +++ b/cmd/shoppingbasketapp/command/root.go @@ -0,0 +1 @@ +package command diff --git a/cmd/shoppingbasketapp/command/serve.go b/cmd/shoppingbasketapp/command/serve.go new file mode 100644 index 00000000..d47dcf0d --- /dev/null +++ b/cmd/shoppingbasketapp/command/serve.go @@ -0,0 +1 @@ +package command diff --git a/cmd/shoppingbasketapp/main.go b/cmd/shoppingbasketapp/main.go new file mode 100644 index 00000000..db04842d --- /dev/null +++ b/cmd/shoppingbasketapp/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/logger" + "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" + "time" +) + +func main() { + cfg := shoppingbasketapp.Config{ + Redis: redis.Config{ + Host: "localhost", + Port: 6379, + Password: "", + DB: 0, + }, + Repo: repository.Config{ + KartKeyPrefix: "shopping-basket-cart:", + TTL: 3600 * time.Second, + }, + HTTPServer: httpserver.Config{ + Host: "localhost", + Port: 8080, + ShutdownTimeout: 10 * time.Second, + }, + Logger: logger.Config{ + FilePath: "logs/shoppingbasket/service.log", + UseLocalTime: true, + FileMaxSizeInMB: 10, + FileMaxAgeInDays: 30, + }, + } + + app, err := shoppingbasketapp.Setup(cfg) + if err != nil { + panic(fmt.Sprintf("error initialize to setup app: %s", err.Error())) + } + + app.Start() +} diff --git a/deploy/shoppingbasket/development/.env.example b/deploy/shoppingbasket/development/.env.example new file mode 100644 index 00000000..e69de29b diff --git a/deploy/shoppingbasket/development/Dockerfile b/deploy/shoppingbasket/development/Dockerfile new file mode 100644 index 00000000..e69de29b diff --git a/deploy/shoppingbasket/development/config.yml b/deploy/shoppingbasket/development/config.yml new file mode 100644 index 00000000..9930ad41 --- /dev/null +++ b/deploy/shoppingbasket/development/config.yml @@ -0,0 +1,20 @@ +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 + +logger: + file_path: "logs/shoppingbasket/service.log" + use_local_time: true + file_max_size_in_mb: 10 + file_max_age_in_days: 30 diff --git a/deploy/shoppingbasket/development/docker-compose.yml b/deploy/shoppingbasket/development/docker-compose.yml new file mode 100644 index 00000000..e69de29b diff --git a/logger/logger.go b/logger/logger.go index cded39e4..13108622 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -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 diff --git a/shoppingbasketapp/app.go b/shoppingbasketapp/app.go index 0f0258e8..734b127c 100644 --- a/shoppingbasketapp/app.go +++ b/shoppingbasketapp/app.go @@ -25,7 +25,7 @@ type Application struct { Logger *slog.Logger } -func (app Application) Setup(cfg Config) (Application, error) { +func Setup(cfg Config) (Application, error) { l := logger.New(cfg.Logger, &slog.HandlerOptions{ Level: slog.LevelDebug, }) From 10dbee28bde25807da8413f23e0f0a0ada9bf1bd Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Tue, 14 Apr 2026 16:55:32 +0330 Subject: [PATCH 09/11] changed log file path --- cmd/shoppingbasketapp/main.go | 2 +- deploy/shoppingbasket/development/config.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/shoppingbasketapp/main.go b/cmd/shoppingbasketapp/main.go index db04842d..98de2812 100644 --- a/cmd/shoppingbasketapp/main.go +++ b/cmd/shoppingbasketapp/main.go @@ -28,7 +28,7 @@ func main() { ShutdownTimeout: 10 * time.Second, }, Logger: logger.Config{ - FilePath: "logs/shoppingbasket/service.log", + FilePath: "cmd/shoppingbasketapp/logs/service.log", UseLocalTime: true, FileMaxSizeInMB: 10, FileMaxAgeInDays: 30, diff --git a/deploy/shoppingbasket/development/config.yml b/deploy/shoppingbasket/development/config.yml index 9930ad41..7a94a5fc 100644 --- a/deploy/shoppingbasket/development/config.yml +++ b/deploy/shoppingbasket/development/config.yml @@ -14,7 +14,7 @@ http_server: shutdown_context_timeout: 10s logger: - file_path: "logs/shoppingbasket/service.log" + file_path: "cmd/shoppingbasketapp/logs/service.log" use_local_time: true file_max_size_in_mb: 10 file_max_age_in_days: 30 From e72e05e64f95f5b7f6119a4463b86048e4a3019f Mon Sep 17 00:00:00 2001 From: Mohammad Amin Date: Tue, 14 Apr 2026 18:53:15 +0330 Subject: [PATCH 10/11] Implement repository and docker-compose --- patientapp/app.go | 10 +- patientapp/cmd/main.go | 17 +- patientapp/delivery/http/analytic/router.go | 8 +- patientapp/delivery/http/analytic/server.go | 7 +- patientapp/docker/docker-compose.yml | 32 ++ .../repository/migration/patient_schema.sql | 481 ++++++++++++++++++ patientapp/repository/grpc/analytic_repo.go | 35 -- patientapp/repository/mysql/analytic_repo.go | 370 +++++++++++++- patientapp/service/analytic/param.go | 28 +- patientapp/service/analytic/patient_filter.go | 8 +- patientapp/service/analytic/service.go | 18 +- patientapp/service/entity/address.go | 4 +- patientapp/service/entity/map_summary.go | 9 +- patientapp/service/entity/patient.go | 23 + 14 files changed, 957 insertions(+), 93 deletions(-) create mode 100644 patientapp/docker/docker-compose.yml create mode 100644 patientapp/docker/repository/migration/patient_schema.sql delete mode 100644 patientapp/repository/grpc/analytic_repo.go diff --git a/patientapp/app.go b/patientapp/app.go index dce0558e..c2450e4c 100644 --- a/patientapp/app.go +++ b/patientapp/app.go @@ -3,17 +3,17 @@ package patientapp import ( "git.gocasts.ir/ebhomengo/niki/patientapp/config" "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic" - "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" "github.com/labstack/echo/v4" ) type Application struct { //Config Config HTTPServer *config.EchoServer - DB *mysql.DataBase + DB *mysql.DB } -func Setup(cfg config.Config, conn *mysql.DataBase) Application { +func Setup(cfg config.Config, db *mysql.DB) Application { e := echo.New() @@ -25,13 +25,13 @@ func Setup(cfg config.Config, conn *mysql.DataBase) Application { return Application{ //Config: config, HTTPServer: &server, - DB: conn, + DB: db, } } func (a Application) Start() { - server := analytic.NewServer(a.HTTPServer) + server := analytic.NewServer(a.HTTPServer, a.DB) _ = server.Serve() } diff --git a/patientapp/cmd/main.go b/patientapp/cmd/main.go index 0c28ac82..0f755aad 100644 --- a/patientapp/cmd/main.go +++ b/patientapp/cmd/main.go @@ -1,15 +1,26 @@ package main import ( + "os" + "strconv" "time" "git.gocasts.ir/ebhomengo/niki/patientapp" "git.gocasts.ir/ebhomengo/niki/patientapp/config" - "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" ) func main() { - db := mysql.DataBase{} + dbConf := mysql.Config{ + Username: os.Getenv("DB_USER"), + Password: os.Getenv("DB_PASS"), + Host: os.Getenv("DB_HOST"), + DBName: os.Getenv("DB_NAME"), + } + port, _ := strconv.Atoi(os.Getenv("DB_PORT")) + dbConf.Port = port + + db := mysql.New(dbConf) cfg := config.Config{ Port: 8080, @@ -19,7 +30,7 @@ func main() { ShutDownCtxTimeout: 5 * time.Second, } - app := patientapp.Setup(cfg, &db) + app := patientapp.Setup(cfg, db) app.Start() diff --git a/patientapp/delivery/http/analytic/router.go b/patientapp/delivery/http/analytic/router.go index c6499428..56ed1136 100644 --- a/patientapp/delivery/http/analytic/router.go +++ b/patientapp/delivery/http/analytic/router.go @@ -1,15 +1,15 @@ package analytic import ( - "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql" + repo "git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql" analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" "github.com/labstack/echo/v4" ) -func NewPatientAnalyticRouter(s *echo.Group) { +func NewPatientAnalyticRouter(s *echo.Group, db *mysql.DB) { - mysqlRepo := mysql.NewPatientRepo() - //rpcRepo := grpc.NewPatientRepo() + mysqlRepo := repo.NewPatientRepo(db) analyticService := analytic2.NewPatientAnalyticService(mysqlRepo) diff --git a/patientapp/delivery/http/analytic/server.go b/patientapp/delivery/http/analytic/server.go index 91ec8ba7..d92e59fc 100644 --- a/patientapp/delivery/http/analytic/server.go +++ b/patientapp/delivery/http/analytic/server.go @@ -5,16 +5,19 @@ import ( "fmt" "git.gocasts.ir/ebhomengo/niki/patientapp/config" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" ) type Server struct { HTTPServer *config.EchoServer + Db *mysql.DB } -func NewServer(server *config.EchoServer) *Server { +func NewServer(server *config.EchoServer, db *mysql.DB) *Server { return &Server{ HTTPServer: server, + Db: db, } } @@ -35,7 +38,7 @@ func (s Server) RegisterRoutes() { { // Analytic Group analyticGroup := v1.Group("/analytic") - NewPatientAnalyticRouter(analyticGroup) + NewPatientAnalyticRouter(analyticGroup, s.Db) } } diff --git a/patientapp/docker/docker-compose.yml b/patientapp/docker/docker-compose.yml new file mode 100644 index 00000000..2d45d595 --- /dev/null +++ b/patientapp/docker/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3.9" + +services: + mysql: + image: mysql:8.0 + container_name: patient + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: patient + MYSQL_USER: appuser + MYSQL_PASSWORD: apppass + TZ: Asia/Tehran + command: [ + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci", + "--default-authentication-plugin=mysql_native_password" + ] + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./repository/migration:/docker-entrypoint-initdb.d + networks: + - backend + +volumes: + mysql_data: + +networks: + backend: + driver: bridge \ No newline at end of file diff --git a/patientapp/docker/repository/migration/patient_schema.sql b/patientapp/docker/repository/migration/patient_schema.sql new file mode 100644 index 00000000..39700c33 --- /dev/null +++ b/patientapp/docker/repository/migration/patient_schema.sql @@ -0,0 +1,481 @@ +-- MySQL dump 10.13 Distrib 8.0.29, for Linux (x86_64) +-- +-- Host: localhost Database: patient +-- ------------------------------------------------------ +-- Server version 8.0.29 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `audits` +-- + +DROP TABLE IF EXISTS `audits`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `audits` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_type` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_id` bigint unsigned DEFAULT NULL, + `event` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `auditable_type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `auditable_id` bigint unsigned NOT NULL, + `old_values` text COLLATE utf8mb4_unicode_ci, + `new_values` text COLLATE utf8mb4_unicode_ci, + `url` text COLLATE utf8mb4_unicode_ci, + `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_agent` varchar(1023) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `tags` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `audits_auditable_type_auditable_id_index` (`auditable_type`,`auditable_id`), + KEY `audits_user_id_user_type_index` (`user_id`,`user_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `bandage_user` +-- + +DROP TABLE IF EXISTS `bandage_user`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `bandage_user` ( + `user_id` bigint unsigned NOT NULL, + `bandage_id` bigint unsigned NOT NULL, + `qty` int unsigned NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `bandages` +-- + +DROP TABLE IF EXISTS `bandages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `bandages` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `cities` +-- + +DROP TABLE IF EXISTS `cities`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cities` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `state_id` int unsigned NOT NULL, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=445 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `drug_user` +-- + +DROP TABLE IF EXISTS `drug_user`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `drug_user` ( + `user_id` bigint unsigned NOT NULL, + `drug_id` bigint unsigned NOT NULL, + `qty` int unsigned NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `drugs` +-- + +DROP TABLE IF EXISTS `drugs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `drugs` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `failed_jobs` +-- + +DROP TABLE IF EXISTS `failed_jobs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `failed_jobs` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `connection` text COLLATE utf8mb4_unicode_ci NOT NULL, + `queue` text COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `logs` +-- + +DROP TABLE IF EXISTS `logs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `logs` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `log` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=20739 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `migrations` +-- + +DROP TABLE IF EXISTS `migrations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `migrations` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `migration` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `batch` int NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `model_has_permissions` +-- + +DROP TABLE IF EXISTS `model_has_permissions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `model_has_permissions` ( + `permission_id` bigint unsigned NOT NULL, + `model_type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `model_id` bigint unsigned NOT NULL, + PRIMARY KEY (`permission_id`,`model_id`,`model_type`), + KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`), + CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `model_has_roles` +-- + +DROP TABLE IF EXISTS `model_has_roles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `model_has_roles` ( + `role_id` bigint unsigned NOT NULL, + `model_type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `model_id` bigint unsigned NOT NULL, + PRIMARY KEY (`role_id`,`model_id`,`model_type`), + KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`), + CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `password_resets` +-- + +DROP TABLE IF EXISTS `password_resets`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `password_resets` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint DEFAULT NULL, + `time` int DEFAULT NULL, + `token` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=227 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `permissions` +-- + +DROP TABLE IF EXISTS `permissions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `permissions` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `guard_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `reports` +-- + +DROP TABLE IF EXISTS `reports`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `reports` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint unsigned NOT NULL, + `agent_id` bigint unsigned NOT NULL, + `type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `interviewee` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `interviewee_name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `interviewee_ratio` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `content` text COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `reports_user_id_foreign` (`user_id`), + CONSTRAINT `reports_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=19846 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `role_has_permissions` +-- + +DROP TABLE IF EXISTS `role_has_permissions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `role_has_permissions` ( + `permission_id` bigint unsigned NOT NULL, + `role_id` bigint unsigned NOT NULL, + PRIMARY KEY (`permission_id`,`role_id`), + KEY `role_has_permissions_role_id_foreign` (`role_id`), + CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE, + CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `roles` +-- + +DROP TABLE IF EXISTS `roles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `roles` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `guard_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `states` +-- + +DROP TABLE IF EXISTS `states`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `states` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user_files` +-- + +DROP TABLE IF EXISTS `user_files`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_files` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint unsigned NOT NULL, + `name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `label` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `message` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=40633 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `user_metas` +-- + +DROP TABLE IF EXISTS `user_metas`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `user_metas` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint unsigned NOT NULL, + `birthPlaceState` smallint unsigned NOT NULL DEFAULT '0', + `birthPlaceCity` smallint unsigned NOT NULL DEFAULT '0', + `religion` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `atba` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `nationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `atba_birthPlaceState` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `atba_birthPlaceCity` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `height` smallint unsigned DEFAULT NULL, + `weight` smallint unsigned DEFAULT NULL, + `eyeColor` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `bloodType` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `addressState` smallint unsigned NOT NULL, + `addressCity` smallint unsigned NOT NULL, + `address` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `postalCode` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL, + `phone` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL, + `mobile` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `fMobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mMobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ePhoneName` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ePhoneNumber` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `houseType` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL, + `housePrice` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `houseMortgage` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `houseRent` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `fAlive` tinyint(1) DEFAULT NULL, + `divorced` tinyint(1) NOT NULL DEFAULT '0', + `devorced` tinyint NOT NULL DEFAULT '0', + `fName` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `fBirthDate` date DEFAULT NULL, + `fNationalCode` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `fAtba` tinyint(1) NOT NULL DEFAULT '0', + `fNationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `fJob` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `fEdu` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mAlive` tinyint(1) DEFAULT NULL, + `mName` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mBirthDate` date DEFAULT NULL, + `mNationalCode` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mAtba` tinyint(1) NOT NULL DEFAULT '0', + `mNationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mJob` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mEdu` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ratio` tinyint(1) DEFAULT NULL, + `ratioType` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `liveChild` tinyint unsigned NOT NULL, + `ebChild` tinyint unsigned NOT NULL, + `Edu` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `skills` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `insurance` tinyint(1) NOT NULL, + `insuranceYear` tinyint unsigned DEFAULT NULL, + `insuranceMonth` tinyint unsigned DEFAULT NULL, + `insuranceCoverage` tinyint(1) NOT NULL, + `insuranceCoverageNumber` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `insuranceCoveragBranch` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `insuranceCoverageType` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `supplementaryInsurance` tinyint(1) NOT NULL, + `supplementaryInsuranceName` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `supportOrganization` tinyint(1) NOT NULL, + `supportOrganizationName` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dailyProblemType` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `spouseName` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `spouseBirthDate` date DEFAULT NULL, + `spouseNationalCode` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `spouseAtba` tinyint(1) NOT NULL DEFAULT '0', + `spouseNationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `spouseJob` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `spouseEdu` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `spouseMobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `spouseAlive` tinyint(1) DEFAULT NULL, + `shenasname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `shenasnamePlace` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `user_metas_user_id_foreign` (`user_id`), + CONSTRAINT `user_metas_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=1461 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `username` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `password` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `pic` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, + `birth_date` date DEFAULT NULL, + `gender` tinyint(1) NOT NULL, + `marital` smallint unsigned DEFAULT NULL, + `deceased` date DEFAULT NULL, + `seyyed` tinyint(1) NOT NULL DEFAULT '0', + `profile_number` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `sheba` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `bank_account` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `bank_account_name` varchar(90) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `profile_status` tinyint unsigned DEFAULT NULL, + `medical_information_status` tinyint unsigned DEFAULT NULL, + `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `drug_description` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `bandage_description` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `bandage_period` tinyint unsigned DEFAULT NULL, + `bandage_months` text COLLATE utf8mb4_unicode_ci, + `twoFA_enabled` tinyint(1) NOT NULL DEFAULT '0', + `twoFA_secret` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_active` tinyint(1) NOT NULL DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_username_unique` (`username`), + KEY `users_twofa_enabled_index` (`twoFA_enabled`), + KEY `users_is_active_index` (`is_active`) +) ENGINE=InnoDB AUTO_INCREMENT=1918 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2026-04-14 7:28:21 diff --git a/patientapp/repository/grpc/analytic_repo.go b/patientapp/repository/grpc/analytic_repo.go deleted file mode 100644 index fe598750..00000000 --- a/patientapp/repository/grpc/analytic_repo.go +++ /dev/null @@ -1,35 +0,0 @@ -package grpc - -import ( - "context" - - "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" - "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" -) - -type AnalyticRepository struct{} - -func NewPatientRepo() *AnalyticRepository { - - return &AnalyticRepository{} -} - -func (db *AnalyticRepository) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { - - return nil, nil -} - -func (db *AnalyticRepository) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { - - return 0, nil -} - -func (db *AnalyticRepository) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { - - return nil, nil -} - -func (db *AnalyticRepository) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { - - return nil, nil -} diff --git a/patientapp/repository/mysql/analytic_repo.go b/patientapp/repository/mysql/analytic_repo.go index 223080cf..ff05fe65 100644 --- a/patientapp/repository/mysql/analytic_repo.go +++ b/patientapp/repository/mysql/analytic_repo.go @@ -2,35 +2,385 @@ package mysql import ( "context" + "fmt" "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic" "git.gocasts.ir/ebhomengo/niki/patientapp/service/entity" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" ) -type DataBase struct{} - -func NewPatientRepo() *DataBase { - - return &DataBase{} +type DataBase struct { + conn *mysql.DB } -func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) { +func NewPatientRepo(db *mysql.DB) *DataBase { - return nil, nil + return &DataBase{ + conn: db, + } +} + +func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.UserMeta, error) { + const Op = "repository.mysql.patient.get" + + tx, err := db.conn.Conn().BeginTx(ctx, nil) + if err != nil { + return []entity.UserMeta{}, richerror.New(Op).WithErr(err) + } + defer tx.Rollback() + + query := ` + SELECT + um.id, + um.user_id, + u.birthDate, + u.sex, + um.birthPlaceState, + um.birthPlaceCity, + um.nationality, + um.addressState, + um.addressCity, + um.address, + um.phone, + um.mobile, + um.spouseName, + um.created_at, + um.updated_at + FROM user_metas um + JOIN users u ON u.id = um.user_id + WHERE 1=1 + ` + + args := []any{} + + // Birthdate filters (FROM = born after) + if f.DOBFrom != nil && *f.DOBFrom != "" { + query += " AND u.birth_date >= ?" + args = append(args, *f.DOBFrom) + } + if f.DOBTo != nil && *f.DOBTo != "" { + query += " AND u.birth_date <= ?" + args = append(args, *f.DOBTo) + } + + // Sex + if f.Sex != nil && *f.Sex != "" { + query += " AND u.sex = ?" + args = append(args, *f.Sex) + } + + // Nationality + if f.Nationality != "" { + query += " AND um.nationality = ?" + args = append(args, f.Nationality) + } + + // Address + if f.AddressState != 0 { + query += " AND um.addressState = ?" + args = append(args, f.AddressState) + } + if f.AddressCity != 0 { + query += " AND um.addressCity = ?" + args = append(args, f.AddressCity) + } + + // Search on fields from user_metas and users + if f.Search != nil && *f.Search != "" { + like := "%" + *f.Search + "%" + query += ` + AND ( + um.fName LIKE ? OR + um.mName LIKE ? OR + um.spouseName LIKE ? OR + um.phone LIKE ? OR + um.mobile LIKE ? + ) + ` + args = append(args, like, like, like, like, like) + } + + query += " ORDER BY id DESC" + + if f.Limit > 0 { + query += " LIMIT ?" + args = append(args, f.Limit) + } + if f.Offset > 0 { + query += " OFFSET ?" + args = append(args, f.Offset) + } + + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + + return nil, richerror.New(Op).WithErr(err) + } + defer rows.Close() + + var result []entity.UserMeta + + for rows.Next() { + var u entity.UserMeta + if err := rows.Scan( + &u.ID, &u.UserID, &u.BirthDate, &u.Gender, &u.BirthPlaceState, &u.BirthPlaceCity, + &u.Religion, &u.Nationality, &u.AddressState, &u.AddressCity, + &u.Address, &u.Mobile, &u.SpouseName, &u.SpouseAlive, + &u.CreatedAt, &u.UpdatedAt, + ); err != nil { + return nil, richerror.New(Op).WithErr(err) + } + result = append(result, u) + } + + if err := rows.Err(); err != nil { + return nil, richerror.New(Op).WithErr(err) + } + + return result, nil } func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) { + const Op = "repository.mysql.patient.count" - return 0, nil + tx, err := db.conn.Conn().BeginTx(ctx, nil) + if err != nil { + return 0, richerror.New(Op).WithErr(err) + } + defer tx.Rollback() + + query := ` + SELECT COUNT(*) + FROM user_metas um + JOIN users u ON u.id = um.user_id + WHERE 1=1 + ` + + args := []any{} + + // Birthdate range + if f.DOBFrom != nil && *f.DOBFrom != "" { + query += " AND u.birth_date >= ?" + args = append(args, *f.DOBFrom) + } + if f.DOBTo != nil && *f.DOBTo != "" { + query += " AND u.birth_date <= ?" + args = append(args, *f.DOBTo) + } + + // Sex + if f.Sex != nil && *f.Sex != "" { + query += " AND u.sex = ?" + args = append(args, *f.Sex) + } + + // Nationality + if f.Nationality != "" { + query += " AND um.nationality = ?" + args = append(args, f.Nationality) + } + + // Address + if f.AddressState != 0 { + query += " AND um.addressState = ?" + args = append(args, f.AddressState) + } + if f.AddressCity != 0 { + query += " AND um.addressCity = ?" + args = append(args, f.AddressCity) + } + + // Search + if f.Search != nil && *f.Search != "" { + like := "%" + *f.Search + "%" + query += ` + AND ( + um.fName LIKE ? OR + um.mName LIKE ? OR + um.spouseName LIKE ? OR + um.phone LIKE ? OR + um.mobile LIKE ? + ) + ` + args = append(args, like, like, like, like, like) + } + + var count int + + err = tx.QueryRowContext(ctx, query, args...).Scan(&count) + if err != nil { + return 0, fmt.Errorf("%s: query error: %w", Op, err) + } + + return count, nil } func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { + const Op = "repository.mysql.patient.map_summary" - return nil, nil + tx, err := db.conn.Conn().BeginTx(ctx, nil) + if err != nil { + return nil, richerror.New(Op).WithErr(err) + } + defer tx.Rollback() + + query := ` + SELECT + um.addressCity AS city_id, + ANY_VALUE(um.lat) AS lat, + ANY_VALUE(um.lng) AS lng, + COUNT(*) AS user_count + FROM user_metas um + JOIN users u ON u.id = um.user_id + JOIN cities c ON c.id = um.addressCity + WHERE c.state_id = ? + ` + + args := []any{provinceID} + // Birthdate filters + if f.MinDOB != nil && *f.MinDOB != "" { + query += " AND u.birth_date >= ?" + args = append(args, *f.MinDOB) + } + if f.MaxDOB != nil && *f.MaxDOB != "" { + query += " AND u.birth_date <= ?" + args = append(args, *f.MaxDOB) + } + + // Sex filter + if f.Sex != nil && *f.Sex != "" { + query += " AND u.sex = ?" + args = append(args, *f.Sex) + } + + // Search filter + if f.Search != nil && *f.Search != "" { + like := "%" + *f.Search + "%" + query += ` + AND ( + um.fName LIKE ? OR + um.mName LIKE ? OR + um.spouseName LIKE ? OR + um.phone LIKE ? OR + um.mobile LIKE ? + ) + ` + args = append(args, like, like, like, like, like) + } + + // Group by city + query += " GROUP BY um.addressCity" + + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("%s: query error: %w", Op, err) + } + defer rows.Close() + + result := make(map[uint][]entity.MapSummaryItem) + + for rows.Next() { + var item entity.MapSummaryItem + + err := rows.Scan( + &item.ID, + &item.Latitude, + &item.Longitude, + &item.Count, + ) + if err != nil { + return nil, fmt.Errorf("%s: scan error: %w", Op, err) + } + + result[item.ID] = append(result[item.ID], item) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("%s: rows error: %w", Op, err) + } + + return result, nil } func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) { - return nil, nil + const Op = "repository.mysql.patient.summary_by_province" + + tx, err := db.conn.Conn().BeginTx(ctx, nil) + if err != nil { + return nil, richerror.New(Op).WithErr(err) + } + defer tx.Rollback() + + query := ` + SELECT + c.state_id, + ANY_VALUE(c.lat) AS lat, + ANY_VALUE(c.lng) AS lng, + COUNT(*) AS total + FROM user_metas um + JOIN users u ON u.id = um.user_id + JOIN cities c ON c.id = um.addressCity + WHERE 1 = 1 + ` + args := []any{} + + // Birthdate filters + if f.MinDOB != nil && *f.MinDOB != "" { + query += " AND u.birth_date >= ?" + args = append(args, *f.MinDOB) + } + if f.MaxDOB != nil && *f.MaxDOB != "" { + query += " AND u.birth_date <= ?" + args = append(args, *f.MaxDOB) + } + + // Sex filter + if f.Sex != nil && *f.Sex != "" { + query += " AND u.sex = ?" + args = append(args, *f.Sex) + } + + // Search filter + if f.Search != nil && *f.Search != "" { + like := "%" + *f.Search + "%" + query += ` + AND ( + um.fName LIKE ? OR + um.mName LIKE ? OR + um.spouseName LIKE ? OR + um.phone LIKE ? OR + um.mobile LIKE ? + ) + ` + args = append(args, like, like, like, like, like) + } + + query += " GROUP BY c.state_id" + + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("%s: query error: %w", Op, err) + } + defer rows.Close() + + result := make(map[uint][]entity.MapSummaryItem) + + for rows.Next() { + var item entity.MapSummaryItem + + if err := rows.Scan(&item.ID, &item.Latitude, &item.Longitude, &item.Count); err != nil { + return nil, fmt.Errorf("%s: scan error: %w", Op, err) + } + + result[item.ID] = []entity.MapSummaryItem{item} + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("%s: rows error: %w", Op, err) + } + + return result, nil } diff --git a/patientapp/service/analytic/param.go b/patientapp/service/analytic/param.go index da14af37..1e93eb41 100644 --- a/patientapp/service/analytic/param.go +++ b/patientapp/service/analytic/param.go @@ -6,12 +6,12 @@ import ( type ListPatientAnalyticRequest struct { // All fields are optional - MinAge *int `query:"minAge,omitempty"` - MaxAge *int `query:"maxAge,omitempty"` - Sex *entity.Sex `query:"sex,omitempty"` + MinAge *int `query:"minAge,omitempty"` + MaxAge *int `query:"maxAge,omitempty"` + Sex *string `query:"sex,omitempty"` - City *int64 `query:"city,omitempty"` - Province *int64 `query:"province,omitempty"` + City *uint16 `query:"city,omitempty"` + Province *uint16 `query:"province,omitempty"` Search *string `query:"search,omitempty"` @@ -19,11 +19,11 @@ type ListPatientAnalyticRequest struct { } type PatientAnalyticItem struct { - ID int64 `json:"id"` + ID uint64 `json:"id"` FirstName string `json:"first_name"` LastName string `json:"Last_name"` DateOfBirth string `json:"dob,omitempty"` - Sex entity.Sex `json:"sex"` + Sex string `json:"sex"` Phone string `json:"phone"` Address entity.Address `json:"address"` } @@ -33,17 +33,17 @@ type PatientAnalyticResponse struct { Total int `json:"total"` } -func ToPatientResponse(patient entity.Patient) PatientAnalyticItem { +func ToPatientResponse(patient entity.UserMeta) PatientAnalyticItem { return PatientAnalyticItem{ ID: patient.ID, - FirstName: patient.FirstName, + FirstName: patient.Name, LastName: patient.LastName, - DateOfBirth: patient.DateOfBirth, - Sex: patient.Sex, - Phone: patient.Phone, + DateOfBirth: patient.BirthDate, + Sex: patient.Gender, + Phone: *patient.Mobile, Address: entity.Address{ - ProvinceID: patient.Address.ProvinceID, - CityID: patient.Address.CityID, + ProvinceID: patient.AddressState, + CityID: patient.AddressCity, }, } } diff --git a/patientapp/service/analytic/patient_filter.go b/patientapp/service/analytic/patient_filter.go index c7dd7695..42a8e47d 100644 --- a/patientapp/service/analytic/patient_filter.go +++ b/patientapp/service/analytic/patient_filter.go @@ -7,11 +7,11 @@ import ( type PatientFilter struct { DOBFrom *string // born after DOBTo *string // born before - Sex *entity.Sex + Sex *string - City *int64 - Province *int64 - Country *int64 + Nationality string + AddressState uint16 + AddressCity uint16 Search *string diff --git a/patientapp/service/analytic/service.go b/patientapp/service/analytic/service.go index 518d8756..01f704c3 100644 --- a/patientapp/service/analytic/service.go +++ b/patientapp/service/analytic/service.go @@ -16,7 +16,7 @@ var ( ) type Repository interface { - GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error) + GetPatients(ctx context.Context, f PatientFilter) ([]entity.UserMeta, error) CountPatients(ctx context.Context, f PatientFilter) (int, error) SummaryByCity(ctx context.Context, provinceID uint, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) @@ -41,14 +41,14 @@ func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (Pati dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now()) filter := PatientFilter{ - DOBFrom: dobFrom, - DOBTo: dobTo, - Sex: req.Sex, - City: req.City, - Province: req.Province, - Search: req.Search, - Limit: limit, - Offset: offset, + DOBFrom: dobFrom, + DOBTo: dobTo, + Sex: req.Sex, + AddressCity: *req.City, + AddressState: *req.Province, + Search: req.Search, + Limit: limit, + Offset: offset, } items, err := s.repository.GetPatients(ctx, filter) diff --git a/patientapp/service/entity/address.go b/patientapp/service/entity/address.go index 193531a9..cdc36198 100644 --- a/patientapp/service/entity/address.go +++ b/patientapp/service/entity/address.go @@ -7,8 +7,8 @@ type Address struct { Name string Lat float64 Lon float64 - CityID uint - ProvinceID uint + CityID uint16 + ProvinceID uint16 } type AddressAggregated struct { diff --git a/patientapp/service/entity/map_summary.go b/patientapp/service/entity/map_summary.go index fcfd1284..e0768ae6 100644 --- a/patientapp/service/entity/map_summary.go +++ b/patientapp/service/entity/map_summary.go @@ -9,9 +9,8 @@ const ( ) type MapSummaryItem struct { - LocationID int64 - Name string - Count int - CentroidLat float64 - CentroidLng float64 + ID uint `json:"id"` // city_id OR state_id + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Count int `json:"count"` } diff --git a/patientapp/service/entity/patient.go b/patientapp/service/entity/patient.go index 33857e26..519d9e38 100644 --- a/patientapp/service/entity/patient.go +++ b/patientapp/service/entity/patient.go @@ -1,5 +1,7 @@ package entity +import "time" + type Patient struct { ID int64 FirstName string @@ -15,6 +17,27 @@ type Patient struct { EndDate string } +type UserMeta struct { + ID uint64 `db:"id"` + UserID uint64 `db:"user_id"` + Name string `db:"name"` + LastName string `db:"last_name"` + Gender string `db:"gender"` + BirthDate string `db:"birth_date"` + BirthPlaceState uint16 `db:"birthPlaceState"` + BirthPlaceCity uint16 `db:"birthPlaceCity"` + Religion *string `db:"religion"` + Nationality *string `db:"nationality"` + AddressState uint16 `db:"addressState"` + AddressCity uint16 `db:"addressCity"` + Address string `db:"address"` + Mobile *string `db:"mobile"` + SpouseName *string `db:"spouseName"` + SpouseAlive *bool `db:"spouseAlive"` + CreatedAt *time.Time `db:"created_at"` + UpdatedAt *time.Time `db:"updated_at"` +} + // Sex ================================== Sex type ========================================== type Sex string From db13725994adb61e765be13dd795f16a4488e9e0 Mon Sep 17 00:00:00 2001 From: mzfarshad Date: Tue, 14 Apr 2026 19:19:12 +0330 Subject: [PATCH 11/11] refactor cmd layer --- cmd/shoppingbasketapp/command/root.go | 51 +++++++++++++++++++ cmd/shoppingbasketapp/command/serve.go | 42 +++++++++++++++ cmd/shoppingbasketapp/main.go | 40 ++------------- .../{config.yml => config.local.yml} | 9 +++- shoppingbasketapp/app.go | 38 ++++++-------- shoppingbasketapp/config.go | 2 +- shoppingbasketapp/service/cart/service.go | 19 ++++--- 7 files changed, 130 insertions(+), 71 deletions(-) rename deploy/shoppingbasket/development/{config.yml => config.local.yml} (60%) diff --git a/cmd/shoppingbasketapp/command/root.go b/cmd/shoppingbasketapp/command/root.go index d47dcf0d..e2634743 100644 --- a/cmd/shoppingbasketapp/command/root.go +++ b/cmd/shoppingbasketapp/command/root.go @@ -1 +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 +} diff --git a/cmd/shoppingbasketapp/command/serve.go b/cmd/shoppingbasketapp/command/serve.go index d47dcf0d..8196c023 100644 --- a/cmd/shoppingbasketapp/command/serve.go +++ b/cmd/shoppingbasketapp/command/serve.go @@ -1 +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) +} diff --git a/cmd/shoppingbasketapp/main.go b/cmd/shoppingbasketapp/main.go index 98de2812..d204ab4a 100644 --- a/cmd/shoppingbasketapp/main.go +++ b/cmd/shoppingbasketapp/main.go @@ -1,44 +1,12 @@ package main import ( - "fmt" - "git.gocasts.ir/ebhomengo/niki/adapter/redis" - "git.gocasts.ir/ebhomengo/niki/logger" - "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" - "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp" - "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" - "time" + "git.gocasts.ir/ebhomengo/niki/cmd/shoppingbasketapp/command" + "os" ) func main() { - cfg := shoppingbasketapp.Config{ - Redis: redis.Config{ - Host: "localhost", - Port: 6379, - Password: "", - DB: 0, - }, - Repo: repository.Config{ - KartKeyPrefix: "shopping-basket-cart:", - TTL: 3600 * time.Second, - }, - HTTPServer: httpserver.Config{ - Host: "localhost", - Port: 8080, - ShutdownTimeout: 10 * time.Second, - }, - Logger: logger.Config{ - FilePath: "cmd/shoppingbasketapp/logs/service.log", - UseLocalTime: true, - FileMaxSizeInMB: 10, - FileMaxAgeInDays: 30, - }, + if err := command.RootCmd.Execute(); err != nil { + os.Exit(1) } - - app, err := shoppingbasketapp.Setup(cfg) - if err != nil { - panic(fmt.Sprintf("error initialize to setup app: %s", err.Error())) - } - - app.Start() } diff --git a/deploy/shoppingbasket/development/config.yml b/deploy/shoppingbasket/development/config.local.yml similarity index 60% rename from deploy/shoppingbasket/development/config.yml rename to deploy/shoppingbasket/development/config.local.yml index 7a94a5fc..eecfaa99 100644 --- a/deploy/shoppingbasket/development/config.yml +++ b/deploy/shoppingbasket/development/config.local.yml @@ -12,9 +12,14 @@ http_server: host: "localhost" port: 8080 shutdown_context_timeout: 10s + cors: + allow_origins: + - "*" + logger: - file_path: "cmd/shoppingbasketapp/logs/service.log" + 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: 30 + file_max_age_in_days: 7 diff --git a/shoppingbasketapp/app.go b/shoppingbasketapp/app.go index 734b127c..2ce4df1e 100644 --- a/shoppingbasketapp/app.go +++ b/shoppingbasketapp/app.go @@ -4,12 +4,11 @@ import ( "context" "fmt" "git.gocasts.ir/ebhomengo/niki/adapter/redis" - logger "git.gocasts.ir/ebhomengo/niki/logger" "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" - "log/slog" "os" "os/signal" "sync" @@ -22,25 +21,21 @@ type Application struct { Handler http.Handler Server http.Server Config Config - Logger *slog.Logger } -func Setup(cfg Config) (Application, error) { - l := logger.New(cfg.Logger, &slog.HandlerOptions{ - Level: slog.LevelDebug, - }) +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, l, repo) + svc := cart.New(validator, repo) handler := http.NewHandler(svc) httpServer, err := httpserver.New(cfg.HTTPServer) if err != nil { - l.Error("failed to initialize http server", "error", err) + logger.L().Error("failed to initialize http server", "error", err) return Application{}, err } server := http.NewServer(handler, httpServer) @@ -51,7 +46,6 @@ func Setup(cfg Config) (Application, error) { Handler: handler, Server: server, Config: cfg, - Logger: l, }, nil } @@ -64,37 +58,37 @@ func (app Application) Start() { startServers(app, &wg) <-ctx.Done() - app.Logger.Info("Shutdown signal received...") + logger.L().Info("Shutdown signal received...") shutdownTimeoutCtx, cancel := context.WithTimeout(context.Background(), app.Config.HTTPServer.ShutdownTimeout) defer cancel() if app.shutdownServers(shutdownTimeoutCtx) { - app.Logger.Info("Servers shutdown gracefully") + logger.L().Info("Servers shutdown gracefully") } else { - app.Logger.Warn("Shutdown timed out, exiting application") + logger.L().Warn("Shutdown timed out, exiting application") return } wg.Wait() - app.Logger.Info("shopping-basket-app stopped") + logger.L().Info("shopping-basket-app stopped") } func startServers(app Application, wg *sync.WaitGroup) { wg.Add(1) go func() { defer wg.Wait() - app.Logger.Info(fmt.Sprintf("HTTP server starting on port: %d", app.Config.HTTPServer.Port)) + logger.L().Info(fmt.Sprintf("HTTP server starting on port: %d", app.Config.HTTPServer.Port)) if err := app.Server.Serve(); err != nil { - app.Logger.Error(fmt.Sprintf("error listen and serve HTTP server on port %d", app.Config.HTTPServer.Port)) + logger.L().Error(fmt.Sprintf("error listen and serve HTTP server on port %d", app.Config.HTTPServer.Port)) } - app.Logger.Info(fmt.Sprintf("HTTP server stopped 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 { - app.Logger.Info("Starting server shutdown process...") + logger.L().Info("Starting server shutdown process...") shutdownDone := make(chan struct{}) @@ -105,7 +99,7 @@ func (app Application) shutdownServers(ctx context.Context) bool { shutdownWg.Wait() close(shutdownDone) - app.Logger.Info("All servers have been shut down successfully.") + logger.L().Info("All servers have been shut down successfully.") }() @@ -118,15 +112,15 @@ func (app Application) shutdownServers(ctx context.Context) bool { } func (app Application) shutdownHTTPServe(parentCtx context.Context, wg *sync.WaitGroup) { - app.Logger.Info(fmt.Sprintf("Starting gracefully shutdown for http server on port %d", app.Config.HTTPServer.Port)) + 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 { - app.Logger.Error(fmt.Sprintf("failed http server gracefully shutdown: %v", err)) + logger.L().Error(fmt.Sprintf("failed http server gracefully shutdown: %v", err)) } - app.Logger.Info("Successfully http server gracefully shutdown") + logger.L().Info("Successfully http server gracefully shutdown") } diff --git a/shoppingbasketapp/config.go b/shoppingbasketapp/config.go index 39a0e4af..291c301f 100644 --- a/shoppingbasketapp/config.go +++ b/shoppingbasketapp/config.go @@ -2,8 +2,8 @@ package shoppingbasketapp import ( "git.gocasts.ir/ebhomengo/niki/adapter/redis" - "git.gocasts.ir/ebhomengo/niki/logger" "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" + logger "git.gocasts.ir/ebhomengo/niki/pkg/logger" "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" ) diff --git a/shoppingbasketapp/service/cart/service.go b/shoppingbasketapp/service/cart/service.go index d84a3f73..1e2da37e 100644 --- a/shoppingbasketapp/service/cart/service.go +++ b/shoppingbasketapp/service/cart/service.go @@ -2,9 +2,9 @@ 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" - "log/slog" "time" ) @@ -18,19 +18,18 @@ type Repository interface { type Service struct { validate Validate - logger *slog.Logger repo Repository } -func New(val Validate, logger *slog.Logger, repo Repository) Service { - return Service{validate: val, logger: logger, repo: repo} +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 { - s.logger.Error("shoppingbasket-service-AddToBasket", "error", err) + logger.L().Error("shoppingbasket-service-AddToBasket", "error", err) return err } @@ -46,13 +45,13 @@ func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error { func (s Service) GetCart(ctx context.Context, userID types.ID) (GetCartResponse, error) { const op = "shoppingbasketapp.service.GetCart" if userID < 1 { - s.logger.Error("shoppingbasket-service-GetCart", "error", "user id must be greater than 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 { - s.logger.Error("shoppingbasket-service-GetCart", "error", err) + logger.L().Error("shoppingbasket-service-GetCart", "error", err) return GetCartResponse{}, richerror.New(op).WithErr(err) } @@ -69,7 +68,7 @@ func (s Service) RemoveFromCart(ctx context.Context, req RemoveFromCartRequest) const op = "shoppingbaskerapp.service.RemoveFromCart" if err := s.validate.ValidateRemoveFromCart(req); err != nil { - s.logger.Error("shoppingbasket-service-RemoveFromCart", "error", err) + logger.L().Error("shoppingbasket-service-RemoveFromCart", "error", err) return err } @@ -80,7 +79,7 @@ func (s Service) UpdateQuantity(ctx context.Context, req UpdateQuantityRequest) const op = "shoppingbaskerapp.service.UpdateQuantity" if err := s.validate.ValidateUpdateQuantity(req); err != nil { - s.logger.Error("shoppingbasket-service-UpdateQuantity", "error", err) + logger.L().Error("shoppingbasket-service-UpdateQuantity", "error", err) return err } @@ -95,7 +94,7 @@ func (s Service) ClearCart(ctx context.Context, userID types.ID) error { const op = "shoppingbaskerapp.service.ClearCart" if userID < 1 { - s.logger.Error("shoppingbasket-service-ClearCart", "error", "user id must be greater than 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") }