From c01d3678ae0d05adab9083ffe3371a77a6566db4 Mon Sep 17 00:00:00 2001 From: amir-ys Date: Wed, 29 Apr 2026 01:53:23 +0330 Subject: [PATCH] feat: add product list api --- pkg/err_msg/message.go | 17 +++++++++ productapp/app.go | 7 +++- productapp/delivery/http/handler.go | 37 ++++++++++++++----- productapp/delivery/http/health_check.go | 11 ++++++ productapp/delivery/http/route.go | 1 - productapp/delivery/http/server.go | 16 ++++++--- productapp/repository/database/db.go | 46 +++++++++++++++++++++++- productapp/service/product/entity.go | 20 +++++------ productapp/service/product/message.go | 13 +++++++ productapp/service/product/param.go | 11 ++++++ productapp/service/product/service.go | 37 +++++++++++++++++++ 11 files changed, 191 insertions(+), 25 deletions(-) create mode 100644 productapp/delivery/http/health_check.go delete mode 100644 productapp/delivery/http/route.go create mode 100644 productapp/service/product/message.go diff --git a/pkg/err_msg/message.go b/pkg/err_msg/message.go index 67924a94..c6c4fb81 100644 --- a/pkg/err_msg/message.go +++ b/pkg/err_msg/message.go @@ -1,6 +1,23 @@ package errmsg +type ErrorResponse struct { + Message string `json:"message"` // General error message + Errors map[string]interface{} `json:"errors,omitempty"` // Additional detail of error + InternalErrCode string `json:"internal_error_code,omitempty"` // Custom error code (optional) +} + +func (e ErrorResponse) Error() string { + return e.Message +} + const ( + ErrValidationFailed = "input validation failed" + ErrUnexpectedError = "unexpected error occurred" + ErrInvalidRequestFormat = "invalid request format" + ErrGetUserInfo = "get user info failed" + ErrFailedDecodeBase64 = "decode data to base 64 failed" + ErrFailedUnmarshalJson = "unmarshal data to JSON failed" + ErrUnauthorized = "unauthorized" ErrorMsgAdminNotAllowed = "admin is not allowed" ErrorMsgNotFound = "record not found" ErrorMsgSomethingWentWrong = "something went wrong" diff --git a/productapp/app.go b/productapp/app.go index 53791556..85d86403 100644 --- a/productapp/app.go +++ b/productapp/app.go @@ -3,6 +3,7 @@ package productapp import ( "context" "fmt" + "git.gocasts.ir/ebhomengo/niki/productapp/service/product" "log" httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server" @@ -25,6 +26,7 @@ type Config struct { type Application struct { Config Config HTTPServer *producthttp.Server + productSvc product.Service DB *productdb.DB Echo *echo.Echo } @@ -40,11 +42,14 @@ func Setup(cfg Config) *Application { Router: e, } - srv := producthttp.NewServer(hsrv) + productSvc := product.New(productDB) + + srv := producthttp.NewServer(hsrv, productSvc) srv.RegisterRoutes() return &Application{ Config: cfg, + productSvc: productSvc, HTTPServer: srv, DB: productDB, Echo: e, diff --git a/productapp/delivery/http/handler.go b/productapp/delivery/http/handler.go index e9549c3c..871c7cab 100644 --- a/productapp/delivery/http/handler.go +++ b/productapp/delivery/http/handler.go @@ -1,17 +1,38 @@ package http import ( - "net/http" - + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + "git.gocasts.ir/ebhomengo/niki/productapp/service/product" "github.com/labstack/echo/v4" + "log/slog" + "net/http" ) -type Handler struct{} - -func NewHandler() *Handler { - return &Handler{} +type Handler struct { + productService product.Service + Logger *slog.Logger } -func (h Handler) HealthCheck(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) +func NewHandler(productService product.Service) *Handler { + return &Handler{ + productService: productService, + } +} + +func (h Handler) getProductList(c echo.Context) error { + var req product.GetProductListRequest + + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, errmsg.ErrorResponse{ + Message: errmsg.ErrInvalidRequestFormat, + }) + } + + response, err := h.productService.GetProducts(c.Request().Context(), req) + if err != nil { + // todo handle validation error + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, response) } diff --git a/productapp/delivery/http/health_check.go b/productapp/delivery/http/health_check.go new file mode 100644 index 00000000..9f08f770 --- /dev/null +++ b/productapp/delivery/http/health_check.go @@ -0,0 +1,11 @@ +package http + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +func (h Handler) HealthCheck(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"status": "healthy"}) +} diff --git a/productapp/delivery/http/route.go b/productapp/delivery/http/route.go deleted file mode 100644 index d02cfda6..00000000 --- a/productapp/delivery/http/route.go +++ /dev/null @@ -1 +0,0 @@ -package http diff --git a/productapp/delivery/http/server.go b/productapp/delivery/http/server.go index 2b20c191..5f9042d7 100644 --- a/productapp/delivery/http/server.go +++ b/productapp/delivery/http/server.go @@ -1,16 +1,19 @@ package http -import httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server" +import ( + httpserver "git.gocasts.ir/ebhomengo/niki/delivery/http_server" + "git.gocasts.ir/ebhomengo/niki/productapp/service/product" +) type Server struct { HTTPServer *httpserver.Server Handler *Handler } -func NewServer(httpserver *httpserver.Server) *Server { +func NewServer(httpserver *httpserver.Server, productSvc product.Service) *Server { return &Server{ HTTPServer: httpserver, - Handler: NewHandler(), + Handler: NewHandler(productSvc), } } @@ -22,5 +25,10 @@ func (s *Server) Stop() {} func (s *Server) RegisterRoutes() { r := s.HTTPServer.Router - r.GET("health-check", s.Handler.HealthCheck) + r.GET("health-check/1", s.Handler.HealthCheck) + + productGroup := r.Group("/v1/products") + + productGroup.GET("", s.Handler.getProductList) + } diff --git a/productapp/repository/database/db.go b/productapp/repository/database/db.go index 5d07c611..d969dc5e 100644 --- a/productapp/repository/database/db.go +++ b/productapp/repository/database/db.go @@ -1,6 +1,11 @@ package database -import "git.gocasts.ir/ebhomengo/niki/repository/mysql" +import ( + "context" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + "git.gocasts.ir/ebhomengo/niki/productapp/service/product" + "git.gocasts.ir/ebhomengo/niki/repository/mysql" +) type DB struct { conn *mysql.DB @@ -11,3 +16,42 @@ func New(conn *mysql.DB) *DB { conn: conn, } } + +func (db *DB) GetProducts(ctx context.Context, page, pageSize uint) ([]product.Product, error) { + const Op = "domain.repository.mysql.order.get-shipping" + + query := "SELECT * FROM products ORDER BY id DESC limit ? offset ?" + rows, err := db.conn.Conn().Query(query, page, pageSize) + + if rows.Err() != nil { + return []product.Product{}, richerror.New(Op).WithErr(err) + } + + defer rows.Close() + + var products []product.Product + + for rows.Next() { + var s product.Product + + err := rows.Scan( + &s.ID, + &s.Name, + &s.Slug, + &s.Description, + &s.Price, + &s.Stock, + &s.IsActive, + &s.Features, + &s.CreatedAt, + &s.DeletedAt, + ) + if err != nil { + return nil, richerror.New(Op).WithErr(err) + } + + products = append(products, s) + } + + return products, nil +} diff --git a/productapp/service/product/entity.go b/productapp/service/product/entity.go index b3dc5901..72f7c04e 100644 --- a/productapp/service/product/entity.go +++ b/productapp/service/product/entity.go @@ -6,16 +6,16 @@ import ( ) type Product struct { - ID uint - Name string - Slug string - Description string - Price float64 - Stock int - IsActive bool - Features string - CreatedAt time.Time - DeletedAt sql.NullTime + ID uint `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Price float64 `json:"price"` + Stock int `json:"stock"` + IsActive bool `json:"is_active"` + Features string `json:"features"` + CreatedAt time.Time `json:"created_at"` + DeletedAt sql.NullTime `json:"-"` } type ProductImage struct { diff --git a/productapp/service/product/message.go b/productapp/service/product/message.go new file mode 100644 index 00000000..6e6d3b59 --- /dev/null +++ b/productapp/service/product/message.go @@ -0,0 +1,13 @@ +package product + +import "errors" + +var ( + ErrFailedGetProductList = errors.New("failed to get products list") + ErrFailedCreateProduct = errors.New("failed to create product") + ErrFailedFindProduct = errors.New("failed to find product") + ErrCantAccessToUpdateProduct = errors.New("you are not allowed to edit this product") + ErrFailedUpdateProduct = errors.New("failed to update product") + ErrFailedToPreparedStatement = errors.New("failed to prepared statement: %v") + ErrCantAccess = errors.New("you are not allowed to this action") +) diff --git a/productapp/service/product/param.go b/productapp/service/product/param.go index e6ae0918..1774ea9e 100644 --- a/productapp/service/product/param.go +++ b/productapp/service/product/param.go @@ -1 +1,12 @@ package product + +import "git.gocasts.ir/ebhomengo/niki/param" + +type GetProductListRequest struct { + param.PaginationRequest +} + +type GetProductListResponse struct { + Data []Product `json:"data"` + param.PaginationResponse +} diff --git a/productapp/service/product/service.go b/productapp/service/product/service.go index e6ae0918..a372f2e6 100644 --- a/productapp/service/product/service.go +++ b/productapp/service/product/service.go @@ -1 +1,38 @@ package product + +import ( + "context" + "fmt" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" +) + +type Repository interface { + GetProducts(ctx context.Context, page, pageSize uint) ([]Product, error) +} + +type Service struct { + repository Repository +} + +func New(repo Repository) Service { + return Service{ + repository: repo, + } +} + +func (s Service) GetProducts(ctx context.Context, req GetProductListRequest) (GetProductListResponse, error) { + + page := req.GetPageNumber() + pageSize := req.PageSize + + products, err := s.repository.GetProducts(ctx, page, pageSize) + if err != nil { + fmt.Println(err) + return GetProductListResponse{}, errmsg.ErrorResponse{ + Message: ErrFailedGetProductList.Error(), + Errors: map[string]interface{}{"repository_error": err.Error()}, + } + } + + return GetProductListResponse{Data: products}, nil +}