From 06d7b4764f9961d9534c113721335c13881e547f Mon Sep 17 00:00:00 2001 From: "aditya.siregar" Date: Sat, 26 Apr 2025 12:23:12 +0700 Subject: [PATCH] update --- config/configs.go | 4 + config/crypto.go | 5 + config/jwt.go | 1 + infra/enaklopos.development.yaml | 3 + internal/common/mycontext/kinoscontext.go | 8 + internal/entity/jwt.go | 7 + internal/entity/order.go | 30 +-- internal/entity/product.go | 1 + internal/entity/user.go | 9 + internal/handlers/http/customerauth/auth.go | 149 +----------- internal/handlers/http/menu.go | 213 ++++++++++++++++++ internal/handlers/http/oss/oss.go | 2 +- internal/handlers/http/product/product.go | 9 +- internal/handlers/http/sites/sites.go | 3 - internal/handlers/request/context.go | 1 - internal/handlers/request/order.go | 35 ++- internal/handlers/response/product.go | 3 - internal/middlewares/auth.go | 31 +++ internal/repository/crypto/init.go | 40 ++++ internal/repository/customer_repo.go | 3 +- internal/repository/models/customer.go | 2 +- internal/repository/orde_repo.go | 76 +++++-- internal/repository/product_repo.go | 36 +++ internal/repository/repository.go | 2 + internal/routes/customer_routes.go | 6 +- internal/services/service.go | 5 + internal/services/v2/auth/auth.go | 58 +++++ .../v2/inprogress_order/in_progress_order.go | 28 ++- .../services/v2/order/create_order_inquiry.go | 6 +- internal/services/v2/order/order.go | 1 + .../services/v2/product/get_product_by_id.go | 10 + internal/services/v2/product/product.go | 2 + 32 files changed, 581 insertions(+), 208 deletions(-) create mode 100644 internal/handlers/http/menu.go create mode 100644 internal/services/v2/auth/auth.go diff --git a/config/configs.go b/config/configs.go index 802c370..02f03c8 100644 --- a/config/configs.go +++ b/config/configs.go @@ -82,5 +82,9 @@ func (c *Config) Auth() *AuthConfig { secret: c.Jwt.TokenWithdraw.Secret, expireTTL: c.Jwt.TokenWithdraw.ExpiresTTL, }, + jwtCustomer: JWT{ + secret: c.Jwt.TokenCustomer.Secret, + expireTTL: c.Jwt.TokenCustomer.ExpiresTTL, + }, } } diff --git a/config/crypto.go b/config/crypto.go index c740a22..8c28839 100644 --- a/config/crypto.go +++ b/config/crypto.go @@ -9,6 +9,7 @@ type AuthConfig struct { jwtOrderExpiresTTL int jwtSecretResetPassword JWT jwtWithdraw JWT + jwtCustomer JWT } type JWT struct { @@ -24,6 +25,10 @@ func (c *AuthConfig) AccessTokenOrderSecret() string { return c.jwtOrderSecret } +func (c *AuthConfig) AccessTokenCustomerSecret() string { + return c.jwtCustomer.secret +} + func (c *AuthConfig) AccessTokenWithdrawSecret() string { return c.jwtWithdraw.secret } diff --git a/config/jwt.go b/config/jwt.go index f8441c1..d5b27da 100644 --- a/config/jwt.go +++ b/config/jwt.go @@ -5,6 +5,7 @@ type Jwt struct { TokenOrder Token `mapstructure:"token-order"` TokenResetPassword Token `mapstructure:"token-reset-password"` TokenWithdraw Token `mapstructure:"token-withdraw"` + TokenCustomer Token `mapstructure:"token-customer"` } type Token struct { diff --git a/infra/enaklopos.development.yaml b/infra/enaklopos.development.yaml index 06849d5..599676a 100644 --- a/infra/enaklopos.development.yaml +++ b/infra/enaklopos.development.yaml @@ -13,6 +13,9 @@ jwt: token-withdraw: expires-ttl: 2 secret: "909Lm25V3Qd7aut8dr4QUxm5PZUrSFs" + token-customer: + expires-ttl: 1400 + secret: "WakLm25V3Qd7aut8dr4QUxm5PZUrWa#" postgresql: host: 62.72.45.250 diff --git a/internal/common/mycontext/kinoscontext.go b/internal/common/mycontext/kinoscontext.go index 45805c4..42645d3 100644 --- a/internal/common/mycontext/kinoscontext.go +++ b/internal/common/mycontext/kinoscontext.go @@ -81,6 +81,14 @@ func NewMyContext(parent context.Context, claims *entity.JWTAuthClaims) (*MyCont }, nil } +func NewMyContextCustomer(parent context.Context, claims *entity.JWTAuthClaimsCustomer) (*MyContextImpl, error) { + return &MyContextImpl{ + Context: parent, + requestedBy: claims.UserID, + name: claims.Name, + }, nil +} + func NewContext(parent context.Context) *MyContextImpl { return &MyContextImpl{ Context: parent, diff --git a/internal/entity/jwt.go b/internal/entity/jwt.go index adb8b2b..da32770 100644 --- a/internal/entity/jwt.go +++ b/internal/entity/jwt.go @@ -13,6 +13,13 @@ type JWTAuthClaims struct { jwt.StandardClaims } +type JWTAuthClaimsCustomer struct { + UserID int64 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + jwt.StandardClaims +} + type JWTOrderClaims struct { PartnerID int64 `json:"id"` OrderID int64 `json:"order_id"` diff --git a/internal/entity/order.go b/internal/entity/order.go index 3f0dce9..fd2ce97 100644 --- a/internal/entity/order.go +++ b/internal/entity/order.go @@ -75,18 +75,19 @@ func (Order) TableName() string { } type OrderItem struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:order_item_id"` - OrderID int64 `gorm:"type:int;column:order_id"` - ItemID int64 `gorm:"type:int;column:item_id"` - ItemType string `gorm:"type:varchar;column:item_type"` - Price float64 `gorm:"type:numeric;not null;column:price"` - Quantity int `gorm:"type:int;column:quantity"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` - CreatedBy int64 `gorm:"type:int;column:created_by"` - UpdatedBy int64 `gorm:"type:int;column:updated_by"` - Product *Product `gorm:"foreignKey:ItemID;references:ID"` - ItemName string `gorm:"type:varchar;column:item_name"` + ID int64 `gorm:"primaryKey;autoIncrement;column:order_item_id"` + OrderID int64 `gorm:"type:int;column:order_id"` + ItemID int64 `gorm:"type:int;column:item_id"` + ItemType string `gorm:"type:varchar;column:item_type"` + Price float64 `gorm:"type:numeric;not null;column:price"` + Quantity int `gorm:"type:int;column:quantity"` + CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` + CreatedBy int64 `gorm:"type:int;column:created_by"` + UpdatedBy int64 `gorm:"type:int;column:updated_by"` + Product *Product `gorm:"foreignKey:ItemID;references:ID"` + ItemName string `gorm:"type:varchar;column:item_name"` + Description string `gorm:"type:varchar;column:description"` } func (OrderItem) TableName() string { @@ -110,8 +111,9 @@ type OrderRequest struct { } type OrderItemRequest struct { - ProductID int64 `json:"product_id" validate:"required"` - Quantity int `json:"quantity" validate:"required"` + ProductID int64 `json:"product_id" validate:"required"` + Quantity int `json:"quantity" validate:"required"` + Description string `json:"description"` } type OrderExecuteRequest struct { diff --git a/internal/entity/product.go b/internal/entity/product.go index 6ba1c3e..4808263 100644 --- a/internal/entity/product.go +++ b/internal/entity/product.go @@ -30,6 +30,7 @@ type ProductSearch struct { Name string Type product.ProductType BranchID int64 + PartnerID int64 Available product.ProductStock Limit int Offset int diff --git a/internal/entity/user.go b/internal/entity/user.go index 8b66731..684fd54 100644 --- a/internal/entity/user.go +++ b/internal/entity/user.go @@ -125,3 +125,12 @@ func (c Customer) HashedPassword() string { return string(hashedPassword) } + +func (u *Customer) ToUserAuthenticate(signedToken string) *AuthenticateUser { + return &AuthenticateUser{ + ID: u.ID, + Token: signedToken, + Name: u.Name, + UserType: u.UserType, + } +} diff --git a/internal/handlers/http/customerauth/auth.go b/internal/handlers/http/customerauth/auth.go index f79d6d4..5f6a736 100644 --- a/internal/handlers/http/customerauth/auth.go +++ b/internal/handlers/http/customerauth/auth.go @@ -2,8 +2,8 @@ package customerauth import ( auth2 "enaklo-pos-be/internal/handlers/request" + "enaklo-pos-be/internal/services/v2/auth" "enaklo-pos-be/internal/services/v2/customer" - "fmt" "net/http" "strings" @@ -15,7 +15,7 @@ import ( ) type AuthHandler struct { - service services.Auth + service auth.Service userService services.User customerSvc customer.Service } @@ -23,49 +23,29 @@ type AuthHandler struct { func (a *AuthHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { authRoute := group.Group("/auth") authRoute.POST("/login", a.AuthLogin) - authRoute.POST("/forgot-password", a.ForgotPassword) - authRoute.POST("/reset-password", jwt, a.ResetPassword) - authRoute.POST("/register", a.Register) - authRoute.POST("/verify", a.VerifyRegistration) } -func NewAuthHandler(service services.Auth, userService services.User, customerSvc customer.Service) *AuthHandler { +func NewAuthHandler(service auth.Service) *AuthHandler { return &AuthHandler{ - service: service, - userService: userService, - customerSvc: customerSvc, + service: service, } } -// AuthLogin handles the authentication process for user login. -// @Summary User login -// @Description Authenticates a user based on the provided credentials and returns a JWT token. -// @Accept json -// @Produce json -// @Param bodyParam body auth2.LoginRequest true "User login credentials" -// @Success 200 {object} response.BaseResponse{data=response.LoginResponse} "Login successful" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/auth/login [post] -// @Tags Auth Login API's func (h *AuthHandler) AuthLogin(c *gin.Context) { + ctx := auth2.GetMyContext(c) + var bodyParam auth2.LoginRequest if err := c.ShouldBindJSON(&bodyParam); err != nil { response.ErrorWrapper(c, errors.ErrorBadRequest) return } email := strings.ToLower(bodyParam.Email) - authUser, err := h.service.AuthenticateUser(c, email, bodyParam.Password) + authUser, err := h.service.AuthCustomer(ctx, email, bodyParam.Password) if err != nil { response.ErrorWrapper(c, err) return } - if authUser.UserType != "CUSTOMER" { - response.ErrorWrapper(c, errors.ErrorUserIsNotFound) - return - } - resp := response.LoginResponseCustoemr{ ID: authUser.ID, Token: authUser.Token, @@ -80,118 +60,3 @@ func (h *AuthHandler) AuthLogin(c *gin.Context) { Data: resp, }) } - -// ForgotPassword handles the request for password reset. -// @Summary Request password reset -// @Description Sends a password reset link to the user's email. -// @Accept json -// @Produce json -// @Param bodyParam body auth2.ForgotPasswordRequest true "User email" -// @Success 200 {object} response.BaseResponse "Password reset link sent" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Router /api/v1/auth/forgot-password [post] -// @Tags Auth Password API's -func (h *AuthHandler) ForgotPassword(c *gin.Context) { - var bodyParam auth2.ResetPasswordRequest - if err := c.ShouldBindJSON(&bodyParam); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - err := h.service.SendPasswordResetLink(c, bodyParam.Email) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Password reset link sent", - }) -} - -// ResetPassword handles the password reset process. -// @Summary Reset user password -// @Description Resets the user's password using the provided token. -// @Accept json -// @Produce json -// @Param bodyParam body auth2.ResetPasswordRequest true "Reset password details" -// @Success 200 {object} response.BaseResponse "Password reset successful" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Router /api/v1/auth/reset-password [post] -// @Tags Auth Password API's -func (h *AuthHandler) ResetPassword(c *gin.Context) { - ctx := auth2.GetMyContext(c) - - var req auth2.ResetPasswordChangeRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - if err := req.Validate(); err != nil { - response.ErrorWrapper(c, errors.NewError( - errors.ErrorBadRequest.ErrorType(), - fmt.Sprintf("invalid request %v", err.Error()))) - return - } - - err := h.service.ResetPassword(ctx, req.OldPassword, req.NewPassword) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Password reset successful", - }) -} - -func (h *AuthHandler) Register(c *gin.Context) { - var req auth2.CustomerRegister - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := auth2.GetMyContext(c) - customer, err := h.customerSvc.RegistrationMember(ctx, req.ToEntity()) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.CustomerRegistrationResp{ - EmailVerificationRequired: true, - PhoneVerificationRequired: false, - VerificationID: customer.VerificationID, - }, - }) -} - -func (h *AuthHandler) VerifyRegistration(c *gin.Context) { - var req auth2.VerifyEmailRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := auth2.GetMyContext(c) - err := h.customerSvc.VerifyOTP(ctx, req.VerificationID, req.OTPCode) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Email verification successful", - }) -} diff --git a/internal/handlers/http/menu.go b/internal/handlers/http/menu.go new file mode 100644 index 0000000..c58c31e --- /dev/null +++ b/internal/handlers/http/menu.go @@ -0,0 +1,213 @@ +package http + +import ( + "enaklo-pos-be/internal/common/errors" + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/handlers/request" + "enaklo-pos-be/internal/handlers/response" + "enaklo-pos-be/internal/services/v2/inprogress_order" + "enaklo-pos-be/internal/services/v2/product" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "net/http" + "strconv" +) + +type MenuHandler struct { + service product.Service + orderService inprogress_order.InProgressOrderService +} + +func NewMenuHandler(service product.Service, orderService inprogress_order.InProgressOrderService, +) *MenuHandler { + return &MenuHandler{ + service: service, + orderService: orderService, + } +} + +func (h *MenuHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { + route := group.Group("/menu") + + route.GET("/:partner_id", h.GetProducts) + route.POST("/order/create", h.OrderCreate) + route.POST("/order/member/create", jwt, h.OrderMemberCreate) + route.GET("/order", h.GetOrderID) +} + +func (h *MenuHandler) GetProducts(c *gin.Context) { + ctx := request.GetMyContext(c) + + partnerIDParam := c.Param("partner_id") + partnerID, err := strconv.ParseInt(partnerIDParam, 10, 64) + if err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + if partnerID <= 0 { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + var req request.ProductParam + if err := c.ShouldBindQuery(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + searchParam := req.ToEntity() + searchParam.PartnerID = partnerID + + products, total, err := h.service.GetProductsByPartnerID(ctx, searchParam) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: h.toProductResponseList(products, int64(total), req), + }) +} + +func (h *MenuHandler) toProductResponseList(resp []*entity.Product, total int64, req request.ProductParam) response.ProductList { + var products []response.Product + for _, b := range resp { + products = append(products, h.toProductResponse(b)) + } + + return response.ProductList{ + Products: products, + Total: total, + Limit: req.Limit, + Offset: req.Offset, + } +} + +func (h *MenuHandler) toProductResponse(resp *entity.Product) response.Product { + return response.Product{ + ID: resp.ID, + Name: resp.Name, + Type: resp.Type, + Price: resp.Price, + Status: resp.Status, + Description: resp.Description, + Image: resp.Image, + } +} + +func (h *MenuHandler) OrderCreate(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req request.OrderCustomer + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + validate := validator.New() + if err := validate.Struct(req); err != nil { + response.ErrorWrapper(c, err) + return + } + + if req.PartnerID == 0 { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + orderRequest := req.ToEntity(req.PartnerID, 0) + + order, err := h.orderService.Save(ctx, orderRequest) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: MapToOrderCreateResponse(order), + }) +} + +func (h *MenuHandler) OrderMemberCreate(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req request.OrderCustomer + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + validate := validator.New() + if err := validate.Struct(req); err != nil { + response.ErrorWrapper(c, err) + return + } + + if req.PartnerID == 0 { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + userID := ctx.RequestedBy() + orderRequest := req.ToEntity(req.PartnerID, userID) + orderRequest.CustomerID = &userID + + order, err := h.orderService.Save(ctx, orderRequest) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: MapToOrderCreateResponse(order), + }) +} + +type GetOrderParam struct { + PartnerID int64 `form:"partner_id" json:"partner_id"` + OrderID int64 `form:"order_id" json:"order_id"` +} + +func (h *MenuHandler) GetOrderID(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req GetOrderParam + if err := c.ShouldBindQuery(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + order, err := h.orderService.GetOrderByOrderAndPartnerID(ctx, req.PartnerID, req.OrderID) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: MapToOrderCreateResponse(order), + }) +} + +func MapToOrderCreateResponse(result *entity.Order) response.OrderResponse { + resp := response.OrderResponse{ + ID: result.ID, + Status: result.Status, + Amount: result.Amount, + Tax: result.Tax, + Total: result.Total, + PaymentType: result.PaymentType, + CreatedAt: result.CreatedAt, + Items: response.MapToOrderItemResponses(result.OrderItems), + } + + return resp +} diff --git a/internal/handlers/http/oss/oss.go b/internal/handlers/http/oss/oss.go index 1559f7e..d6445a1 100644 --- a/internal/handlers/http/oss/oss.go +++ b/internal/handlers/http/oss/oss.go @@ -22,7 +22,7 @@ type OssHandler struct { func (h *OssHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { route := group.Group("/file") - route.POST("/upload", h.UploadFile) + route.POST("/upload", jwt, h.UploadFile) } func NewOssHandler(ossService services.OSSService) *OssHandler { diff --git a/internal/handlers/http/product/product.go b/internal/handlers/http/product/product.go index 0b7ed88..d0e15d8 100644 --- a/internal/handlers/http/product/product.go +++ b/internal/handlers/http/product/product.go @@ -6,12 +6,10 @@ import ( "enaklo-pos-be/internal/handlers/request" "enaklo-pos-be/internal/handlers/response" "enaklo-pos-be/internal/services" - "net/http" - "strconv" - "time" - "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" + "net/http" + "strconv" ) type Handler struct { @@ -275,9 +273,6 @@ func (h *Handler) toProductResponse(resp *entity.Product) response.Product { Price: resp.Price, Status: resp.Status, Description: resp.Description, - CreatedAt: resp.CreatedAt.Format(time.RFC3339), - UpdatedAt: resp.CreatedAt.Format(time.RFC3339), - PartnerID: resp.PartnerID, Image: resp.Image, } } diff --git a/internal/handlers/http/sites/sites.go b/internal/handlers/http/sites/sites.go index 81ed864..5f7571a 100644 --- a/internal/handlers/http/sites/sites.go +++ b/internal/handlers/http/sites/sites.go @@ -313,14 +313,11 @@ func (h *Handler) toProductResponseList(products []entity.Product) []response.Pr for _, product := range products { res = append(res, response.Product{ ID: product.ID, - PartnerID: product.PartnerID, Name: product.Name, Type: product.Type, Price: product.Price, Status: product.Status, Description: product.Description, - CreatedAt: product.CreatedAt.Format(time.RFC3339), - UpdatedAt: product.UpdatedAt.Format(time.RFC3339), }) } return res diff --git a/internal/handlers/request/context.go b/internal/handlers/request/context.go index 305cd9f..e44b5fa 100644 --- a/internal/handlers/request/context.go +++ b/internal/handlers/request/context.go @@ -8,7 +8,6 @@ import ( func GetMyContext(c *gin.Context) mycontext.Context { rawCtx, exists := c.Get("myCtx") if !exists { - // handle missing context return mycontext.NewContext(c) } diff --git a/internal/handlers/request/order.go b/internal/handlers/request/order.go index 349a23f..9ef0044 100644 --- a/internal/handlers/request/order.go +++ b/internal/handlers/request/order.go @@ -14,6 +14,16 @@ type Order struct { PaymentProvider string `json:"payment_provider"` TableNumber string `json:"table_number"` OrderItems []OrderItem `json:"order_items"` + PartnerID int64 `json:"partner_id"` +} + +type OrderCustomer struct { + CustomerName string `json:"customer_name" validate:"required"` + CustomerPhone string `json:"customer_phone"` + CustomerEmail string `json:"customer_email"` + TableNumber string `json:"table_number" validate:"required"` + OrderItems []OrderItem `json:"order_items" validate:"required"` + PartnerID int64 `json:"partner_id" validate:"required"` } type CustomerOrder struct { @@ -82,8 +92,9 @@ func (o *OrderParam) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch { } type OrderItem struct { - ProductID int64 `json:"product_id" validate:"required"` - Quantity int `json:"quantity" validate:"required"` + ProductID int64 `json:"product_id" validate:"required"` + Quantity int `json:"quantity" validate:"required"` + Description string `json:"description"` } func (o *Order) ToEntity(partnerID, createdBy int64) *entity.OrderRequest { @@ -143,3 +154,23 @@ func (o *OrderParamCustomer) ToOrderEntity(ctx mycontext.Context) entity.OrderSe type OrderPrintDetail struct { ID int64 `form:"id" json:"id" example:"10"` } + +func (o *OrderCustomer) ToEntity(partnerID, createdBy int64) *entity.OrderRequest { + orderItems := make([]entity.OrderItemRequest, len(o.OrderItems)) + for i, item := range o.OrderItems { + orderItems[i] = entity.OrderItemRequest{ + ProductID: item.ProductID, + Quantity: item.Quantity, + Description: item.Description, + } + } + + return &entity.OrderRequest{ + PartnerID: partnerID, + OrderItems: orderItems, + CreatedBy: createdBy, + Source: "ONLINE_ORDER", + CustomerName: o.CustomerName, + TableNumber: o.TableNumber, + } +} diff --git a/internal/handlers/response/product.go b/internal/handlers/response/product.go index 83c002f..33d26cb 100644 --- a/internal/handlers/response/product.go +++ b/internal/handlers/response/product.go @@ -2,14 +2,11 @@ package response type Product struct { ID int64 `json:"id"` - PartnerID int64 `json:"partner_id"` Name string `json:"name"` Type string `json:"type"` Price float64 `json:"price"` Status string `json:"status"` Description string `json:"description"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` Image string `json:"image"` } diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go index 8ee9f97..21e633e 100644 --- a/internal/middlewares/auth.go +++ b/internal/middlewares/auth.go @@ -41,6 +41,37 @@ func AuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc { } } +func CustomerAuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc { + return func(c *gin.Context) { + tokenString := c.GetHeader("Authorization") + if tokenString == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + c.Abort() + return + } + + tokenString = strings.TrimPrefix(tokenString, "Bearer ") + + claims, err := cryp.ParseAndValidateJWTCustomer(tokenString) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid JWT token"}) + c.Abort() + return + } + + customCtx, err := mycontext.NewMyContextCustomer(c.Request.Context(), claims) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "error initialize context"}) + c.Abort() + return + } + + c.Set("myCtx", customCtx) + + c.Next() + } +} + func SuperAdminMiddleware() gin.HandlerFunc { return func(c *gin.Context) { ctx, exists := c.Get("myCtx") diff --git a/internal/repository/crypto/init.go b/internal/repository/crypto/init.go index 6215aeb..ecdb4c7 100644 --- a/internal/repository/crypto/init.go +++ b/internal/repository/crypto/init.go @@ -27,6 +27,7 @@ type CryptoConfig interface { AccessTokenResetPasswordExpire() time.Time AccessTokenWithdrawSecret() string AccessTokenWithdrawExpire() time.Time + AccessTokenCustomerSecret() string } type CryptoImpl struct { @@ -126,6 +127,21 @@ func (c *CryptoImpl) ParseAndValidateJWT(tokenString string) (*entity.JWTAuthCla } } +func (c *CryptoImpl) ParseAndValidateJWTCustomer(tokenString string) (*entity.JWTAuthClaimsCustomer, error) { + token, err := jwt.ParseWithClaims(tokenString, &entity.JWTAuthClaimsCustomer{}, func(token *jwt.Token) (interface{}, error) { + return []byte(c.Config.AccessTokenCustomerSecret()), nil + }) + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*entity.JWTAuthClaimsCustomer); ok && token.Valid { + return claims, nil + } else { + return nil, errors.ErrorUnauthorized + } +} + func (c *CryptoImpl) GenerateJWTOrder(order *entity.Order) (string, error) { claims := &entity.JWTOrderClaims{ StandardClaims: jwt.StandardClaims{ @@ -282,3 +298,27 @@ func (c *CryptoImpl) ValidateJWTWithdraw(tokenString string) (*entity.WalletWith Fee: claims.Fee, }, nil } + +func (c *CryptoImpl) GenerateJWTCustomer(user *entity.Customer) (string, error) { + claims := &entity.JWTAuthClaimsCustomer{ + StandardClaims: jwt.StandardClaims{ + Subject: strconv.FormatInt(user.ID, 10), + ExpiresAt: c.Config.AccessTokenExpiresDate().Unix(), + IssuedAt: time.Now().Unix(), + NotBefore: time.Now().Unix(), + }, + UserID: user.ID, + Name: user.Name, + Email: user.Email, + } + + token, err := jwt. + NewWithClaims(jwt.SigningMethodHS256, claims). + SignedString([]byte(c.Config.AccessTokenCustomerSecret())) + + if err != nil { + return "", err + } + + return token, nil +} diff --git a/internal/repository/customer_repo.go b/internal/repository/customer_repo.go index 9afdcac..67a6eb4 100644 --- a/internal/repository/customer_repo.go +++ b/internal/repository/customer_repo.go @@ -157,7 +157,7 @@ func (r *customerRepository) AddPoints(ctx mycontext.Context, customerID int64, Reference: reference, PointsEarned: points, TransactionDate: time.Now(), - Status: "SUCCESS", + Status: "active", } if err := tx.Create(&pointTransaction).Error; err != nil { @@ -255,6 +255,7 @@ func (r *customerRepository) toDomainCustomerModel(dbModel *models.CustomerDB) * UpdatedAt: dbModel.UpdatedAt, CustomerID: dbModel.CustomerID, BirthDate: dbModel.BirthDate, + Password: dbModel.Password, } } diff --git a/internal/repository/models/customer.go b/internal/repository/models/customer.go index a80c83d..e21e609 100644 --- a/internal/repository/models/customer.go +++ b/internal/repository/models/customer.go @@ -50,7 +50,7 @@ func (CustomerPointsDB) TableName() string { type CustomerPointTransactionDB struct { ID uint64 `gorm:"column:id;primaryKey;autoIncrement"` CustomerID int64 `gorm:"column:customer_id;not null"` - Reference string `gorm:"column:transaction_id"` + Reference string `gorm:"column:reference"` PointsEarned int `gorm:"column:points_earned;not null"` TransactionDate time.Time `gorm:"column:transaction_date;not null"` ExpirationDate *time.Time `gorm:"column:expiration_date"` diff --git a/internal/repository/orde_repo.go b/internal/repository/orde_repo.go index e68da62..fde50e3 100644 --- a/internal/repository/orde_repo.go +++ b/internal/repository/orde_repo.go @@ -37,6 +37,7 @@ type OrderRepository interface { ctx mycontext.Context, req entity.PopularProductsRequest, ) ([]entity.PopularProductItem, error) + FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) } type orderRepository struct { @@ -60,16 +61,32 @@ func (r *orderRepository) Create(ctx mycontext.Context, order *entity.Order) (*e } }() - if err := tx.Create(&orderDB).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to insert order") - } + if order.InProgressOrderID != 0 { + orderDB.ID = order.InProgressOrderID - order.ID = orderDB.ID + if err := tx.Omit("customer_id", "partner_id", "customer_name", "created_by").Save(&orderDB).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to update in-progress order") + } + + order.ID = order.InProgressOrderID + + if err := tx.Where("order_id = ?", order.ID).Delete(&models.OrderItemDB{}).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to delete existing order items") + } + } else { + if err := tx.Create(&orderDB).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to insert order") + } + + order.ID = orderDB.ID + } for i := range order.OrderItems { item := &order.OrderItems[i] - item.OrderID = orderDB.ID + item.OrderID = order.ID itemDB := r.toOrderItemDBModel(item) @@ -81,23 +98,19 @@ func (r *orderRepository) Create(ctx mycontext.Context, order *entity.Order) (*e item.ID = itemDB.ID } - if order.InProgressOrderID != 0 { - if err := tx.Where("order_id = ?", order.InProgressOrderID).Delete(&models.OrderItemDB{}).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to delete in-progress order items") - } - - if err := tx.Where("id = ?", order.InProgressOrderID).Delete(&models.OrderDB{}).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to delete in-progress order") - } - } - + // Commit the transaction if err := tx.Commit().Error; err != nil { return nil, errors.Wrap(err, "failed to commit transaction") } - return order, nil + var updatedOrderDB models.OrderDB + if err := r.db.Preload("OrderItems").First(&updatedOrderDB, order.ID).Error; err != nil { + return nil, errors.Wrap(err, "failed to fetch updated order") + } + + updatedOrder := r.toDomainOrderModel(&updatedOrderDB) + + return updatedOrder, nil } func (r *orderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) { @@ -690,7 +703,7 @@ func (r *orderRepository) GetSalesByCategory( ctx mycontext.Context, req entity.SalesByCategoryRequest, ) ([]entity.SalesByCategoryItem, error) { - var salesByCategory []entity.SalesByCategoryItem + salesByCategory := []entity.SalesByCategoryItem{} baseQuery := r.db.Model(&models.OrderItemDB{}). Joins("JOIN orders ON order_items.order_id = orders.id"). @@ -792,8 +805,7 @@ func (r *orderRepository) GetPopularProducts( return nil, errors.Wrap(err, "failed to calculate total sales") } - // Prepare the query for popular products - var popularProducts []entity.PopularProductItem + popularProducts := []entity.PopularProductItem{} orderClause := "total_sales DESC" if req.SortBy == "revenue" { orderClause = "total_revenue DESC" @@ -824,3 +836,23 @@ func (r *orderRepository) GetPopularProducts( return popularProducts, nil } + +func (r *orderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) { + var orderDB models.OrderDB + + if err := r.db.Preload("OrderItems").Where("id = ? AND partner_id = ?", id, partnerID).First(&orderDB).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("order not found") + } + return nil, errors.Wrap(err, "failed to find order") + } + + order := r.toDomainOrderModel(&orderDB) + + for _, itemDB := range orderDB.OrderItems { + item := r.toDomainOrderItemModel(&itemDB) + order.OrderItems = append(order.OrderItems, *item) + } + + return order, nil +} diff --git a/internal/repository/product_repo.go b/internal/repository/product_repo.go index a40c89f..a639811 100644 --- a/internal/repository/product_repo.go +++ b/internal/repository/product_repo.go @@ -11,6 +11,7 @@ import ( type ProductRepository interface { GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) + GetProductsByPartnerID(ctx mycontext.Context, req entity.ProductSearch) ([]*entity.Product, int64, error) } type productRepository struct { @@ -78,5 +79,40 @@ func (r *productRepository) toDomainProductModel(dbModel *models.ProductDB) *ent Status: dbModel.Status, CreatedAt: dbModel.CreatedAt, UpdatedAt: dbModel.UpdatedAt, + Image: dbModel.Image, } } + +func (r *productRepository) GetProductsByPartnerID(ctx mycontext.Context, req entity.ProductSearch) ([]*entity.Product, int64, error) { + if req.PartnerID == 0 { + return nil, 0, nil + } + query := r.db.Where("partner_id = ?", req.PartnerID) + + if req.Type != "" { + query = query.Where("type = ?", req.Type) + } + + if req.Name != "" { + query = query.Where("name ILIKE ?", "%"+req.Name+"%") + } + + var total int64 + if err := query.Model(&models.ProductDB{}).Count(&total).Error; err != nil { + return nil, 0, errors.Wrap(err, "failed to count products") + } + + var productsDB []models.ProductDB + + if err := query.Find(&productsDB).Error; err != nil { + return nil, 0, errors.Wrap(err, "failed to find products") + } + + products := make([]*entity.Product, 0, len(productsDB)) + for i := range productsDB { + product := r.toDomainProductModel(&productsDB[i]) + products = append(products, product) + } + + return products, total, nil +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index cb0f2b6..3914c28 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -120,6 +120,8 @@ type Crypto interface { ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error) GenerateJWTWithdraw(req *entity.WalletWithdrawRequest) (string, error) ValidateJWTWithdraw(tokenString string) (*entity.WalletWithdrawRequest, error) + GenerateJWTCustomer(user *entity.Customer) (string, error) + ParseAndValidateJWTCustomer(tokenString string) (*entity.JWTAuthClaimsCustomer, error) } type User interface { diff --git a/internal/routes/customer_routes.go b/internal/routes/customer_routes.go index ae86dd1..155214a 100644 --- a/internal/routes/customer_routes.go +++ b/internal/routes/customer_routes.go @@ -1,6 +1,7 @@ package routes import ( + "enaklo-pos-be/internal/handlers/http" "enaklo-pos-be/internal/handlers/http/customerauth" "enaklo-pos-be/internal/handlers/http/discovery" "enaklo-pos-be/internal/middlewares" @@ -14,11 +15,12 @@ func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceMan repoManager *repository.RepoManagerImpl) { approute := app.Group("/api/v1/customer") - authMiddleware := middlewares.AuthorizationMiddleware(repoManager.Crypto) + authMiddleware := middlewares.CustomerAuthorizationMiddleware(repoManager.Crypto) serverRoutes := []HTTPHandlerRoutes{ discovery.NewHandler(serviceManager.DiscoverService), - customerauth.NewAuthHandler(serviceManager.AuthSvc, serviceManager.UserSvc, serviceManager.CustomerV2Svc), + customerauth.NewAuthHandler(serviceManager.AuthV2Svc), + http.NewMenuHandler(serviceManager.ProductV2Svc, serviceManager.InProgressSvc), } for _, handler := range serverRoutes { diff --git a/internal/services/service.go b/internal/services/service.go index 0caf205..c4e79c4 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -14,9 +14,11 @@ import ( "enaklo-pos-be/internal/services/studio" "enaklo-pos-be/internal/services/transaction" "enaklo-pos-be/internal/services/users" + authSvc "enaklo-pos-be/internal/services/v2/auth" customerSvc "enaklo-pos-be/internal/services/v2/customer" "enaklo-pos-be/internal/services/v2/inprogress_order" orderSvc "enaklo-pos-be/internal/services/v2/order" + "enaklo-pos-be/internal/services/v2/partner_settings" productSvc "enaklo-pos-be/internal/services/v2/product" @@ -48,6 +50,7 @@ type ServiceManagerImpl struct { ProductV2Svc productSvc.Service MemberRegistrationSvc member.RegistrationService InProgressSvc inprogress_order.InProgressOrderService + AuthV2Svc authSvc.Service } func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl { @@ -76,6 +79,8 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) MemberRegistrationSvc: member.NewMemberRegistrationService(repo.MemberRepository, repo.EmailService, custSvcV2), CustomerV2Svc: custSvcV2, InProgressSvc: inprogressOrder, + ProductV2Svc: productSvcV2, + AuthV2Svc: authSvc.New(repo.CustomerRepo, repo.Crypto), } } diff --git a/internal/services/v2/auth/auth.go b/internal/services/v2/auth/auth.go new file mode 100644 index 0000000..6d8e271 --- /dev/null +++ b/internal/services/v2/auth/auth.go @@ -0,0 +1,58 @@ +package auth + +import ( + "enaklo-pos-be/internal/common/errors" + "enaklo-pos-be/internal/common/logger" + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" + "go.uber.org/zap" +) + +type Repository interface { + FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) +} + +type CryptoSvc interface { + GenerateJWTCustomer(user *entity.Customer) (string, error) + CompareHashAndPassword(hash string, password string) bool +} + +type Service interface { + AuthCustomer(ctx mycontext.Context, email, password string) (*entity.AuthenticateUser, error) +} + +type authSvc struct { + repo Repository + crypt CryptoSvc +} + +func New(repo Repository, cryptSvc CryptoSvc) Service { + return &authSvc{ + repo: repo, + crypt: cryptSvc, + } +} + +func (a authSvc) AuthCustomer(ctx mycontext.Context, email, password string) (*entity.AuthenticateUser, error) { + user, err := a.repo.FindByEmail(ctx, email) + if err != nil { + logger.ContextLogger(ctx).Error("error when get user", zap.Error(err)) + return nil, errors.ErrorInternalServer + } + + if user == nil { + return nil, errors.ErrorUserIsNotFound + } + + if ok := a.crypt.CompareHashAndPassword(user.Password, password); !ok { + return nil, errors.ErrorUserInvalidLogin + } + + signedToken, err := a.crypt.GenerateJWTCustomer(user) + + if err != nil { + return nil, err + } + + return user.ToUserAuthenticate(signedToken), nil +} diff --git a/internal/services/v2/inprogress_order/in_progress_order.go b/internal/services/v2/inprogress_order/in_progress_order.go index 0ea834d..8345787 100644 --- a/internal/services/v2/inprogress_order/in_progress_order.go +++ b/internal/services/v2/inprogress_order/in_progress_order.go @@ -13,11 +13,13 @@ import ( type InProgressOrderService interface { Save(ctx mycontext.Context, order *entity.OrderRequest) (*entity.Order, error) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) + GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) } type OrderRepository interface { CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) + FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) } type OrderCalculator interface { @@ -26,6 +28,7 @@ type OrderCalculator interface { items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, + partnerID int64, ) (*entity.OrderCalculation, error) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) } @@ -57,7 +60,7 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques return nil, err } - orderCalculation, err := s.orderCalculator.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source) + orderCalculation, err := s.orderCalculator.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source, req.PartnerID) if err != nil { return nil, err } @@ -71,11 +74,12 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques } orderItems[i] = entity.OrderItem{ - ItemID: item.ProductID, - ItemName: productName, - Quantity: item.Quantity, - Price: product.Price, - ItemType: product.Type, + ItemID: item.ProductID, + ItemName: productName, + Quantity: item.Quantity, + Price: product.Price, + ItemType: product.Type, + Description: product.Description, } } @@ -119,3 +123,15 @@ func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partner return orders, nil } + +func (s *inProgressOrderSvc) GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) { + orders, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) + if err != nil { + logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID", + zap.Error(err), + zap.Int64("partnerID", partnerID)) + return nil, errors.Wrap(err, "failed to get order") + } + + return orders, nil +} diff --git a/internal/services/v2/order/create_order_inquiry.go b/internal/services/v2/order/create_order_inquiry.go index 9e6f5e5..81d1d57 100644 --- a/internal/services/v2/order/create_order_inquiry.go +++ b/internal/services/v2/order/create_order_inquiry.go @@ -24,7 +24,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context, return nil, err } - orderCalculation, err := s.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source) + orderCalculation, err := s.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source, req.PartnerID) if err != nil { return nil, err } @@ -104,6 +104,7 @@ func (s *orderSvc) CalculateOrderTotals( items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, + partnerID int64, ) (*entity.OrderCalculation, error) { subtotal := 0.0 @@ -115,8 +116,7 @@ func (s *orderSvc) CalculateOrderTotals( subtotal += product.Price * float64(item.Quantity) } - partnerID := ctx.GetPartnerID() - setting, err := s.partnerSetting.GetSettings(ctx, *partnerID) + setting, err := s.partnerSetting.GetSettings(ctx, partnerID) if err != nil { return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "failed to get partner settings") diff --git a/internal/services/v2/order/order.go b/internal/services/v2/order/order.go index 966a82c..e568003 100644 --- a/internal/services/v2/order/order.go +++ b/internal/services/v2/order/order.go @@ -67,6 +67,7 @@ type Service interface { items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, + partnerID int64, ) (*entity.OrderCalculation, error) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) GetOrderPaymentAnalysis( diff --git a/internal/services/v2/product/get_product_by_id.go b/internal/services/v2/product/get_product_by_id.go index 49074fc..c2e77f9 100644 --- a/internal/services/v2/product/get_product_by_id.go +++ b/internal/services/v2/product/get_product_by_id.go @@ -31,3 +31,13 @@ func (s *productSvc) GetProductsByIDs(ctx mycontext.Context, ids []int64, partne return products, nil } + +func (s *productSvc) GetProductsByPartnerID(ctx mycontext.Context, search entity.ProductSearch) ([]*entity.Product, int64, error) { + products, total, err := s.repo.GetProductsByPartnerID(ctx, search) + + if err != nil { + return nil, 0, errors.Wrap(err, "failed to get products by partner ID") + } + + return products, total, nil +} diff --git a/internal/services/v2/product/product.go b/internal/services/v2/product/product.go index 8f7125d..02a0d1b 100644 --- a/internal/services/v2/product/product.go +++ b/internal/services/v2/product/product.go @@ -8,11 +8,13 @@ import ( type Repository interface { GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) + GetProductsByPartnerID(ctx mycontext.Context, req entity.ProductSearch) ([]*entity.Product, int64, error) } type Service interface { GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) + GetProductsByPartnerID(ctx mycontext.Context, search entity.ProductSearch) ([]*entity.Product, int64, error) } type productSvc struct {