This commit is contained in:
aditya.siregar 2025-04-26 12:23:12 +07:00
parent 09c9a4d59d
commit 06d7b4764f
32 changed files with 581 additions and 208 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"`

View File

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

View File

@ -30,6 +30,7 @@ type ProductSearch struct {
Name string
Type product.ProductType
BranchID int64
PartnerID int64
Available product.ProductStock
Limit int
Offset int

View File

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

View File

@ -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",
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")

View File

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

View File

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

View File

@ -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"`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")

View File

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

View File

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

View File

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