From bdf930445ca33d1adfb0f1bb3e6a8f058f0a4307 Mon Sep 17 00:00:00 2001 From: "aditya.siregar" Date: Sun, 4 Aug 2024 01:14:59 +0700 Subject: [PATCH] Customer Order --- internal/constants/role/role.go | 1 + internal/constants/transaction/transaction.go | 1 + internal/entity/auth.go | 2 + internal/entity/discovery.go | 3 + internal/entity/midtrans.go | 1 + internal/entity/order.go | 6 + internal/entity/product.go | 27 +- internal/entity/sites.go | 5 + internal/entity/user.go | 4 + internal/handlers/http/auth/auth.go | 5 + internal/handlers/http/customerauth/auth.go | 176 ++++++++++++ internal/handlers/http/customerorder/order.go | 252 ++++++++++++++++++ internal/handlers/http/discovery/discover.go | 104 +++++++- internal/handlers/request/discovery.go | 4 + internal/handlers/request/order.go | 47 ++++ internal/handlers/request/user.go | 27 ++ internal/handlers/response/auth.go | 7 + internal/handlers/response/discovery.go | 32 +++ internal/handlers/response/order.go | 21 ++ internal/handlers/response/user.go | 11 + internal/repository/brevo/init.go | 8 - internal/repository/midtrans/init.go | 10 +- internal/repository/orders/order.go | 18 +- internal/repository/products/product.go | 10 + internal/repository/repository.go | 1 + internal/repository/users/user.go | 12 +- internal/routes/customer_routes.go | 4 + internal/services/discovery/discovery.go | 54 +++- internal/services/order/order.go | 17 ++ internal/services/service.go | 5 +- 30 files changed, 832 insertions(+), 43 deletions(-) create mode 100644 internal/handlers/http/customerauth/auth.go create mode 100644 internal/handlers/http/customerorder/order.go diff --git a/internal/constants/role/role.go b/internal/constants/role/role.go index defaefb..99c8dc4 100644 --- a/internal/constants/role/role.go +++ b/internal/constants/role/role.go @@ -8,4 +8,5 @@ const ( PartnerAdmin Role = 3 SiteAdmin Role = 4 Casheer Role = 5 + Customer Role = 6 ) diff --git a/internal/constants/transaction/transaction.go b/internal/constants/transaction/transaction.go index d034ea4..b7e0120 100644 --- a/internal/constants/transaction/transaction.go +++ b/internal/constants/transaction/transaction.go @@ -19,6 +19,7 @@ const ( Debit PaymentMethod = "DEBIT" Transfer PaymentMethod = "TRANSFER" QRIS PaymentMethod = "QRIS" + Online PaymentMethod = "ONLINE" ) func (b PaymentMethod) toString() string { diff --git a/internal/entity/auth.go b/internal/entity/auth.go index 6945a24..d70d53a 100644 --- a/internal/entity/auth.go +++ b/internal/entity/auth.go @@ -85,6 +85,7 @@ func (UserDB) TableName() string { func (u *UserDB) ToUserAuthenticate(signedToken string, license PartnerLicense) *AuthenticateUser { return &AuthenticateUser{ + ID: u.ID, Token: signedToken, Name: u.Name, RoleID: role.Role(u.RoleID), @@ -96,6 +97,7 @@ func (u *UserDB) ToUserAuthenticate(signedToken string, license PartnerLicense) SiteName: u.SiteName, ResetPassword: u.ResetPassword, PartnerLicense: license, + UserType: u.UserType, } } diff --git a/internal/entity/discovery.go b/internal/entity/discovery.go index 3986c77..7d4bcba 100644 --- a/internal/entity/discovery.go +++ b/internal/entity/discovery.go @@ -37,3 +37,6 @@ type MustVisit struct { Region string `json:"region"` Regency string `json:"regency"` } + +type DiscoveryGetByIDResp struct { +} diff --git a/internal/entity/midtrans.go b/internal/entity/midtrans.go index b26f0d2..b3166c1 100644 --- a/internal/entity/midtrans.go +++ b/internal/entity/midtrans.go @@ -7,6 +7,7 @@ type MidtransResponse struct { type MidtransRequest struct { PaymentReferenceID string + PaymentMethod string TotalAmount int64 OrderItems []OrderItem } diff --git a/internal/entity/order.go b/internal/entity/order.go index 6b14688..c766f5a 100644 --- a/internal/entity/order.go +++ b/internal/entity/order.go @@ -17,6 +17,9 @@ type Order struct { PaymentType string `gorm:"type:varchar;column:payment_type"` UpdatedBy int64 `gorm:"type:int;column:updated_by"` OrderItems []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` + Payment Payment `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` + User User `gorm:"foreignKey:CreatedBy;constraint:OnDelete:CASCADE;"` + Source string `gorm:"type:varchar;column:source"` } type OrderDB struct { @@ -69,6 +72,7 @@ func (OrderItem) TableName() string { } type OrderRequest struct { + Source string CreatedBy int64 PartnerID int64 `json:"partner_id" validate:"required"` PaymentMethod string `json:"payment_method" validate:"required"` @@ -120,6 +124,7 @@ type OrderSearch struct { PartnerID *int64 SiteID *int64 IsAdmin bool + CreatedBy int64 PaymentType string Status string Limit int @@ -127,6 +132,7 @@ type OrderSearch struct { StartDate string EndDate string Period string + IsCustomer bool } type HistoryOrderList []*HistoryOrderDB diff --git a/internal/entity/product.go b/internal/entity/product.go index 0cb94e9..bf93711 100644 --- a/internal/entity/product.go +++ b/internal/entity/product.go @@ -60,18 +60,21 @@ func (ProductDB) TableName() string { func (e *ProductDB) ToProduct() *Product { return &Product{ - ID: e.ID, - Name: e.Name, - Type: e.Type, - Price: e.Price, - Status: e.Status, - Description: e.Description, - PartnerID: e.PartnerID, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - DeletedAt: e.DeletedAt, - CreatedBy: e.CreatedBy, - UpdatedBy: e.UpdatedBy, + ID: e.ID, + Name: e.Name, + Type: e.Type, + Price: e.Price, + Status: e.Status, + Description: e.Description, + PartnerID: e.PartnerID, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + DeletedAt: e.DeletedAt, + CreatedBy: e.CreatedBy, + UpdatedBy: e.UpdatedBy, + SiteID: e.SiteID, + IsSeasonTicket: e.IsSeasonTicket, + IsWeekendTicket: e.IsWeekendTicket, } } diff --git a/internal/entity/sites.go b/internal/entity/sites.go index 7314679..e5be63f 100644 --- a/internal/entity/sites.go +++ b/internal/entity/sites.go @@ -28,6 +28,7 @@ type Site struct { Latitude *float64 `json:"latitude"` Longitude *float64 `json:"longitude"` Region string `json:"region"` + Regency string `json:"regency"` Distance float64 `json:"distance"` } @@ -78,6 +79,10 @@ func (e *SiteDB) ToSite() *Site { DeletedAt: e.DeletedAt, CreatedBy: e.CreatedBy, UpdatedBy: e.UpdatedBy, + Regency: e.Regency, + Region: e.Region, + Latitude: e.Latitude, + Longitude: e.Longitude, Products: e.Products, } } diff --git a/internal/entity/user.go b/internal/entity/user.go index 338a1b3..01a6935 100644 --- a/internal/entity/user.go +++ b/internal/entity/user.go @@ -16,6 +16,7 @@ type User struct { Password string Status userstatus.UserStatus NIK string + UserType string CreatedAt time.Time UpdatedAt time.Time RoleID role.Role @@ -29,6 +30,7 @@ type User struct { } type AuthenticateUser struct { + ID int64 Token string Name string RoleID role.Role @@ -40,6 +42,7 @@ type AuthenticateUser struct { SiteName string ResetPassword bool PartnerLicense PartnerLicense + UserType string } type UserRoleDB struct { @@ -78,6 +81,7 @@ func (u *User) ToUserDB(createdBy int64) (*UserDB, error) { SiteID: u.SiteID, PhoneNumber: u.PhoneNumber, NIK: u.NIK, + UserType: u.UserType, }, nil } diff --git a/internal/handlers/http/auth/auth.go b/internal/handlers/http/auth/auth.go index 159abed..6c3dfd3 100644 --- a/internal/handlers/http/auth/auth.go +++ b/internal/handlers/http/auth/auth.go @@ -54,6 +54,11 @@ func (h *AuthHandler) AuthLogin(c *gin.Context) { return } + if authUser.UserType == "CUSTOMER" { + response.ErrorWrapper(c, errors.ErrorUserIsNotFound) + return + } + var partner *response.Partner var site *response.SiteName diff --git a/internal/handlers/http/customerauth/auth.go b/internal/handlers/http/customerauth/auth.go new file mode 100644 index 0000000..48ef77b --- /dev/null +++ b/internal/handlers/http/customerauth/auth.go @@ -0,0 +1,176 @@ +package customerauth + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + + "furtuna-be/internal/common/errors" + auth2 "furtuna-be/internal/handlers/request" + "furtuna-be/internal/handlers/response" + "furtuna-be/internal/services" +) + +type AuthHandler struct { + service services.Auth + userService services.User +} + +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) +} + +func NewAuthHandler(service services.Auth, userService services.User) *AuthHandler { + return &AuthHandler{ + service: service, + userService: userService, + } +} + +// 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) { + var bodyParam auth2.LoginRequest + if err := c.ShouldBindJSON(&bodyParam); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + authUser, err := h.service.AuthenticateUser(c, bodyParam.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, + Name: authUser.Name, + ResetPassword: authUser.ResetPassword, + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Message: "Login Success", + 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.UserRegister + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + ctx := auth2.GetMyContext(c) + res, err := h.userService.Create(ctx, req.ToEntity()) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + resp := response.UserRegister{ + ID: res.ID, + Name: res.Name, + Email: res.Email, + Status: string(res.Status), + CreatedAt: res.CreatedAt, + UpdatedAt: res.UpdatedAt, + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: resp, + }) +} diff --git a/internal/handlers/http/customerorder/order.go b/internal/handlers/http/customerorder/order.go new file mode 100644 index 0000000..7db90d5 --- /dev/null +++ b/internal/handlers/http/customerorder/order.go @@ -0,0 +1,252 @@ +package customerorder + +import ( + "encoding/json" + "furtuna-be/internal/common/errors" + "furtuna-be/internal/entity" + "furtuna-be/internal/handlers/request" + "furtuna-be/internal/handlers/response" + "furtuna-be/internal/services" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" +) + +type Handler struct { + service services.Order +} + +func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { + route := group.Group("/order") + + route.POST("/inquiry", jwt, h.Inquiry) + route.POST("/execute", jwt, h.Execute) + route.GET("/history", jwt, h.History) + route.GET("/detail", jwt, h.Detail) +} + +func NewHandler(service services.Order) *Handler { + return &Handler{ + service: service, + } +} + +func (h *Handler) Inquiry(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req request.CustomerOrder + 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 + } + + order, err := h.service.CreateOrder(ctx, req.ToEntity(ctx.RequestedBy())) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: MapOrderToCreateOrderResponse(order), + }) +} + +func (h *Handler) Execute(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req request.Execute + 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 + } + + order, err := h.service.Execute(ctx, req.ToOrderExecuteRequest(ctx.RequestedBy())) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: MapOrderToExecuteOrderResponse(order), + }) +} + +func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response.CreateOrderResponse { + order := orderResponse.Order + orderItems := make([]response.CreateOrderItemResponse, len(order.OrderItems)) + for i, item := range order.OrderItems { + orderItems[i] = response.CreateOrderItemResponse{ + ID: item.ID, + ItemID: item.ItemID, + Quantity: item.Quantity, + Price: item.Price, + Name: item.Product.Name, + } + } + + return response.CreateOrderResponse{ + ID: order.ID, + RefID: order.RefID, + PartnerID: order.PartnerID, + Status: order.Status, + Amount: order.Amount, + PaymentType: order.PaymentType, + CreatedAt: order.CreatedAt, + OrderItems: orderItems, + Token: orderResponse.Token, + } +} + +func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse) response.ExecuteOrderResponse { + order := orderResponse.Order + orderItems := make([]response.CreateOrderItemResponse, len(order.OrderItems)) + for i, item := range order.OrderItems { + orderItems[i] = response.CreateOrderItemResponse{ + ID: item.ID, + ItemID: item.ItemID, + Quantity: item.Quantity, + Price: item.Price, + Name: item.Product.Name, + } + } + + return response.ExecuteOrderResponse{ + ID: order.ID, + RefID: order.RefID, + PartnerID: order.PartnerID, + Status: order.Status, + Amount: order.Amount, + PaymentType: order.PaymentType, + CreatedAt: order.CreatedAt, + OrderItems: orderItems, + PaymentToken: orderResponse.PaymentToken, + RedirectURL: orderResponse.RedirectURL, + } +} + +func (h *Handler) toHistoryOrderResponse(resp *entity.HistoryOrder) response.HistoryOrder { + return response.HistoryOrder{ + ID: resp.ID, + Employee: resp.Employee, + Site: resp.Site, + Timestamp: resp.Timestamp.Format(time.RFC3339), + BookingTime: resp.BookingTime.Format(time.RFC3339), + Tickets: resp.Tickets, + PaymentType: resp.PaymentType, + Status: resp.Status, + Amount: resp.Amount, + } +} + +func (h *Handler) toHistoryOrderList(resp []*entity.HistoryOrder, total int64, req request.OrderParamCustomer) response.HistoryOrderList { + var orders []response.HistoryOrder + for _, b := range resp { + orders = append(orders, h.toHistoryOrderResponse(b)) + } + + return response.HistoryOrderList{ + Orders: orders, + Total: total, + Limit: req.Limit, + Offset: req.Offset, + } +} + +func (h *Handler) History(c *gin.Context) { + var req request.OrderParamCustomer + if err := c.ShouldBindQuery(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + ctx := request.GetMyContext(c) + orders, total, err := h.service.GetAllHistoryOrders(ctx, req.ToOrderEntity(ctx)) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: h.toHistoryOrderList(orders, int64(total), req), + }) +} + +func (h *Handler) Detail(c *gin.Context) { + var req request.OrderParamCustomer + if err := c.ShouldBindQuery(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + ctx := request.GetMyContext(c) + order, err := h.service.GetByID(ctx, req.ID) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: h.toOrderDetail(order), + }) +} + +func (h *Handler) toOrderDetail(order *entity.Order) *response.OrderDetail { + if order == nil { + return nil + } + + payment := map[string]string{} + paymentLink := "" + + if order.Payment.RequestMetadata != nil && order.Status != "EXPIRED" { + json.Unmarshal(order.Payment.RequestMetadata, &payment) + paymentLink = payment["payment_redirect_url"] + } + + orderDetail := &response.OrderDetail{ + ID: order.ID, + QRCode: order.RefID, + FullName: order.User.Name, + Email: order.User.Email, + PhoneNumber: order.User.PhoneNumber, + TotalAmount: order.Amount, + CreatedAt: order.CreatedAt, + Status: order.Status, + PaymentLink: paymentLink, + } + + orderDetail.OrderItems = make([]response.OrderDetailItem, len(order.OrderItems)) + for i, item := range order.OrderItems { + orderDetail.OrderItems[i] = response.OrderDetailItem{ + ItemType: item.ItemType, + Description: "", + Quantity: int(item.Quantity), + UnitPrice: item.Price, + TotalPrice: float64(item.Quantity) * item.Price, + } + } + + return orderDetail +} diff --git a/internal/handlers/http/discovery/discover.go b/internal/handlers/http/discovery/discover.go index cee541e..2036379 100644 --- a/internal/handlers/http/discovery/discover.go +++ b/internal/handlers/http/discovery/discover.go @@ -1,14 +1,13 @@ package discovery import ( - "furtuna-be/internal/entity" - "github.com/gin-gonic/gin" - "net/http" - "furtuna-be/internal/common/errors" + "furtuna-be/internal/entity" "furtuna-be/internal/handlers/request" "furtuna-be/internal/handlers/response" "furtuna-be/internal/services" + "github.com/gin-gonic/gin" + "net/http" ) type Handler struct { @@ -20,6 +19,8 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { route.GET("/home", h.DisoveryHome) route.GET("/search", h.DisoverySearch) + route.GET("/site/detail", h.DiscoveryGetByID) + route.GET("/site/products", h.DiscoveryProducts) } @@ -71,6 +72,52 @@ func (h *Handler) DisoverySearch(c *gin.Context) { }) } +func (h *Handler) DiscoveryGetByID(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req request.DiscoverySearchByID + if err := c.ShouldBindQuery(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + res, err := h.service.GetByID(ctx, req.ID) + + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: ConvertEntityToGetByIDResp(res), + }) +} + +func (h *Handler) DiscoveryProducts(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req request.DiscoverySearchByID + if err := c.ShouldBindQuery(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + res, err := h.service.GetProductsByID(ctx, req.ID) + + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: ConvertToProductResp(res), + }) +} + func ConvertEntityToResponse(entityResp *entity.DiscoverySearchResp) *response.ExploreResponse { // Convert ExploreRegions exploreRegions := make([]response.Region, len(entityResp.ExploreRegions)) @@ -132,3 +179,52 @@ func ConvertEntityToSearchResponse(entityResp *entity.DiscoverySearchResp, total Offset: req.Offset, } } + +func ConvertEntityToGetByIDResp(resp *entity.Site) *response.SearchSiteByIDResponse { + if resp == nil { + return nil + } + + return &response.SearchSiteByIDResponse{ + ID: resp.ID, + Name: resp.Name, + Image: resp.Image, + Address: resp.Address, + LocationLink: resp.LocationLink, + Description: resp.Description, + Highlight: resp.Highlight, + ContactPerson: resp.ContactPerson, + TnC: resp.TnC, + AdditionalInfo: resp.AdditionalInfo, + PartnerID: resp.PartnerID, + } +} + +func ConvertToProductResp(resp []*entity.Product) *response.SearchProductSiteResponse { + if resp == nil { + return nil + } + + var productResp []response.SearchProductSiteByIDResponse + + partnerID := int64(0) + for _, res := range resp { + productResp = append(productResp, response.SearchProductSiteByIDResponse{ + ID: res.ID, + Name: res.Name, + SiteID: res.SiteID, + Price: res.Price, + IsWeekendTicket: res.IsWeekendTicket, + IsSeasonTicket: res.IsSeasonTicket, + Description: res.Description, + Type: res.Type, + }) + + partnerID = res.PartnerID + } + + return &response.SearchProductSiteResponse{ + Product: productResp, + PartnerID: partnerID, + } +} diff --git a/internal/handlers/request/discovery.go b/internal/handlers/request/discovery.go index 0f5c0dc..ef10496 100644 --- a/internal/handlers/request/discovery.go +++ b/internal/handlers/request/discovery.go @@ -15,6 +15,10 @@ type DiscoveryHomeParam struct { Discover string `form:"discover" json:"discover" example:"0"` } +type DiscoverySearchByID struct { + ID int64 `form:"id" json:"id" example:"0"` +} + func (d *DiscoveryHomeParam) ToEntity() *entity.DiscoverySearch { if d.Limit == 0 { d.Limit = 10 diff --git a/internal/handlers/request/order.go b/internal/handlers/request/order.go index 63d23fe..86c43ea 100644 --- a/internal/handlers/request/order.go +++ b/internal/handlers/request/order.go @@ -12,6 +12,30 @@ type Order struct { OrderItems []OrderItem `json:"order_items" validate:"required"` } +type CustomerOrder struct { + PartnerID int64 `json:"partner_id" validate:"required"` + PaymentMethod transaction.PaymentMethod `json:"payment_method" validate:"required"` + OrderItems []OrderItem `json:"order_items" validate:"required"` +} + +func (o *CustomerOrder) ToEntity(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, + } + } + + return &entity.OrderRequest{ + PartnerID: o.PartnerID, + PaymentMethod: string(o.PaymentMethod), + OrderItems: orderItems, + CreatedBy: createdBy, + Source: "ONLINE", + } +} + type OrderParam struct { PaymentType string `form:"payment_type" json:"payment_type" example:"CASH"` StartDate string `form:"start_date" json:"start_date"` @@ -56,6 +80,7 @@ func (o *Order) ToEntity(createdBy int64) *entity.OrderRequest { PaymentMethod: string(o.PaymentMethod), OrderItems: orderItems, CreatedBy: createdBy, + Source: "POS", } } @@ -71,3 +96,25 @@ func (e Execute) ToOrderExecuteRequest(createdBy int64) *entity.OrderExecuteRequ Token: e.Token, } } + +type OrderParamCustomer struct { + ID int64 `form:"id" json:"id" example:"10"` + Limit int `form:"limit" json:"limit" example:"10"` + Offset int `form:"offset" json:"offset" example:"0"` +} + +func (o *OrderParamCustomer) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch { + if o.Limit == 0 { + o.Limit = 10 + } + + return entity.OrderSearch{ + PartnerID: ctx.GetPartnerID(), + SiteID: ctx.GetSiteID(), + IsAdmin: ctx.IsAdmin(), + Limit: o.Limit, + Offset: o.Offset, + CreatedBy: ctx.RequestedBy(), + IsCustomer: true, + } +} diff --git a/internal/handlers/request/user.go b/internal/handlers/request/user.go index 73e08b8..ff12a54 100644 --- a/internal/handlers/request/user.go +++ b/internal/handlers/request/user.go @@ -72,3 +72,30 @@ func (p *UserParam) ToEntity(ctx mycontext.Context) entity.UserSearch { Offset: p.Offset, } } + +type UserRegister struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required"` + PhoneNumber string `json:"phone_number" validate:"required"` + Password string `json:"password" validate:"required"` +} + +func (e *UserRegister) Validate() error { + validate := validator.New() + if err := validate.Struct(e); err != nil { + return err + } + + return nil +} + +func (u *UserRegister) ToEntity() *entity.User { + return &entity.User{ + Name: u.Name, + Email: u.Email, + PhoneNumber: u.PhoneNumber, + Password: u.Password, + RoleID: role.Customer, + UserType: "CUSTOMER", + } +} diff --git a/internal/handlers/response/auth.go b/internal/handlers/response/auth.go index e86ee15..64dd299 100644 --- a/internal/handlers/response/auth.go +++ b/internal/handlers/response/auth.go @@ -10,6 +10,13 @@ type LoginResponse struct { PartnerLicense *PartnerLicense `json:"partner_license,omitempty"` } +type LoginResponseCustoemr struct { + ID int64 `json:"id"` + Token string `json:"token"` + Name string `json:"name"` + ResetPassword bool `json:"reset_password"` +} + type Role struct { ID int64 `json:"id"` Role string `json:"role_name"` diff --git a/internal/handlers/response/discovery.go b/internal/handlers/response/discovery.go index 940e412..c53a161 100644 --- a/internal/handlers/response/discovery.go +++ b/internal/handlers/response/discovery.go @@ -47,3 +47,35 @@ type SiteSeach struct { ImageURL string `json:"imageUrl"` Regency string `json:"regency"` } + +type SearchSiteByIDResponse struct { + ID int64 `json:"id"` + Name string `json:"name"` + PartnerID int64 `json:"partner_id"` + Image string `json:"image"` + Address string `json:"address"` + LocationLink string `json:"location_link"` + Description string `json:"description"` + Highlight string `json:"highlight"` + ContactPerson string `json:"contact_person"` + TnC string `json:"tn_c"` + AdditionalInfo string `json:"additional_info"` + Status string `json:"status"` +} + +type SearchProductSiteByIDResponse struct { + ID int64 `json:"id"` + SiteID int64 `json:"site_id"` + Name string `json:"name"` + Type string `json:"type"` + Price float64 `json:"price"` + IsWeekendTicket bool `json:"is_weekend_ticket"` + IsSeasonTicket bool `json:"is_season_ticket"` + Description string `json:"description"` + PartnerID int64 `json:"partner_id"` +} + +type SearchProductSiteResponse struct { + Product []SearchProductSiteByIDResponse `json:"product"` + PartnerID int64 `json:"partner_id"` +} diff --git a/internal/handlers/response/order.go b/internal/handlers/response/order.go index 40b92f8..15e70d2 100644 --- a/internal/handlers/response/order.go +++ b/internal/handlers/response/order.go @@ -124,3 +124,24 @@ type PaymentDistribution struct { PaymentType string `json:"payment_type"` Count int `json:"count"` } + +type OrderDetail struct { + ID int64 `json:"id"` // Order ID + QRCode string `json:"qr_code"` // QR code data (can be a URL or base64 string) + FullName string `json:"full_name"` // Customer's full name + Email string `json:"email"` // Customer's email address + PhoneNumber string `json:"phone_number"` // Customer's phone number + OrderItems []OrderDetailItem `json:"order_items"` // List of ordered items + TotalAmount float64 `json:"total_amount"` // Total amount paid + CreatedAt time.Time `json:"created_at"` // Order creation time + Status string `json:"status"` + PaymentLink string `json:"payment_link"` +} + +type OrderDetailItem struct { + ItemType string `json:"item_type"` + Description string `json:"description"` + Quantity int `json:"quantity"` // Quantity of the item + UnitPrice float64 `json:"unit_price"` // Price per unit + TotalPrice float64 `json:"total_price"` // Total price for this item (Quantity * UnitPrice) +} diff --git a/internal/handlers/response/user.go b/internal/handlers/response/user.go index a14ea22..b4c83a7 100644 --- a/internal/handlers/response/user.go +++ b/internal/handlers/response/user.go @@ -1,5 +1,7 @@ package response +import "time" + type User struct { ID int64 `json:"id"` Name string `json:"name"` @@ -19,3 +21,12 @@ type UserList struct { Limit int `json:"limit"` Offset int `json:"offset"` } + +type UserRegister struct { + ID int64 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} diff --git a/internal/repository/brevo/init.go b/internal/repository/brevo/init.go index 4e4e86b..64ce7df 100644 --- a/internal/repository/brevo/init.go +++ b/internal/repository/brevo/init.go @@ -3,7 +3,6 @@ package brevo import ( "bytes" "context" - "fmt" "furtuna-be/internal/entity" "html/template" "io/ioutil" @@ -77,12 +76,5 @@ func New(conf Config) *ServiceImpl { cfg := brevo.NewConfiguration() cfg.AddDefaultHeader("api-key", conf.GetApiKey()) client := brevo.NewAPIClient(cfg) - result, resp, err := client.AccountApi.GetAccount(context.Background()) - if err != nil { - fmt.Println("Error when calling AccountApi->get_account: ", err.Error()) - log.Fatal("error") - } - fmt.Println("GetAccount Object:", result, " GetAccount Response: ", resp) - return &ServiceImpl{brevoConn: client} } diff --git a/internal/repository/midtrans/init.go b/internal/repository/midtrans/init.go index 253e612..2a73dbd 100644 --- a/internal/repository/midtrans/init.go +++ b/internal/repository/midtrans/init.go @@ -39,10 +39,14 @@ func (c *ClientService) CreatePayment(order entity.MidtransRequest) (*entity.Mid Client: c.client, } + paymentMethod := []midtrans.PaymentType{} + + if order.PaymentMethod == "GOPAY" { + paymentMethod = append(paymentMethod, midtrans.SourceGopay) + } + snapReq := &midtrans.SnapReq{ - EnabledPayments: []midtrans.PaymentType{ - midtrans.SourceGopay, - }, + EnabledPayments: paymentMethod, TransactionDetails: midtrans.TransactionDetails{ OrderID: order.PaymentReferenceID, GrossAmt: order.TotalAmount, diff --git a/internal/repository/orders/order.go b/internal/repository/orders/order.go index 0a8f40e..4595760 100644 --- a/internal/repository/orders/order.go +++ b/internal/repository/orders/order.go @@ -48,9 +48,13 @@ func (r *OrderRepository) UpdateStatus(ctx context.Context, orderID int64, statu func (r *OrderRepository) FindByID(ctx context.Context, id int64) (*entity.Order, error) { var order entity.Order - err := r.db.WithContext(ctx).Preload("OrderItems", func(db *gorm.DB) *gorm.DB { - return db.Preload("Product") - }).First(&order, id).Error + err := r.db.WithContext(ctx). + Preload("OrderItems", func(db *gorm.DB) *gorm.DB { + return db.Preload("Product") + }). + Preload("User"). + Preload("Payment"). + First(&order, id).Error if err != nil { logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err)) @@ -99,11 +103,15 @@ func (b *OrderRepository) GetAllHystoryOrders(ctx context.Context, req entity.Or query = query.Where("orders.payment_type = ?", req.PaymentType) } + if req.CreatedBy != 0 { + query = query.Where("orders.created_by = ?", req.CreatedBy) + } + if req.Status != "" { query = query.Where("orders.status = ?", req.Status) } - if !req.IsAdmin { + if !req.IsAdmin && !req.IsCustomer { query = query.Where("orders.partner_id = ?", req.PartnerID) } @@ -142,7 +150,7 @@ func (b *OrderRepository) GetAllHystoryOrders(ctx context.Context, req entity.Or query = query.Limit(req.Limit) } - if err := query.Scan(&orders).Error; err != nil { + if err := query.Debug().Scan(&orders).Error; err != nil { logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err)) return nil, 0, err } diff --git a/internal/repository/products/product.go b/internal/repository/products/product.go index 60a8496..3c8da1b 100644 --- a/internal/repository/products/product.go +++ b/internal/repository/products/product.go @@ -55,6 +55,16 @@ func (b *ProductRepository) GetProductByPartnerIDAndSiteID(ctx context.Context, return products, nil } +func (b *ProductRepository) GetProductsBySiteID(ctx context.Context, siteID int64) (entity.ProductList, error) { + var products []*entity.ProductDB + if err := b.db.WithContext(ctx).Where("site_id = ?", siteID).Find(&products).Error; err != nil { + logger.ContextLogger(ctx).Error("error when finding product by partner ID and site id", zap.Error(err)) + return nil, err + } + + return products, nil +} + func (b *ProductRepository) GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error) { var products []*entity.ProductDB var total int64 diff --git a/internal/repository/repository.go b/internal/repository/repository.go index a8a8be7..6c321d9 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -135,6 +135,7 @@ type Product interface { GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error) DeleteProduct(ctx context.Context, id int64) error GetProductsByIDs(ctx context.Context, ids []int64, partnerID int64) ([]*entity.ProductDB, error) + GetProductsBySiteID(ctx context.Context, siteID int64) (entity.ProductList, error) } type Order interface { diff --git a/internal/repository/users/user.go b/internal/repository/users/user.go index 4c48e88..8ae8891 100644 --- a/internal/repository/users/user.go +++ b/internal/repository/users/user.go @@ -35,11 +35,13 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.UserDB) (*enti return nil, err } - userRole := user.ToUserRoleDB() - if err := tx.Create(userRole).Error; err != nil { - tx.Rollback() - logError(ctx, "creating user role", err) - return nil, err + if user.UserType != "CUSTOMER" { + userRole := user.ToUserRoleDB() + if err := tx.Create(userRole).Error; err != nil { + tx.Rollback() + logError(ctx, "creating user role", err) + return nil, err + } } if err := tx.Commit().Error; err != nil { diff --git a/internal/routes/customer_routes.go b/internal/routes/customer_routes.go index e9d35cf..179f01f 100644 --- a/internal/routes/customer_routes.go +++ b/internal/routes/customer_routes.go @@ -1,6 +1,8 @@ package routes import ( + "furtuna-be/internal/handlers/http/customerauth" + "furtuna-be/internal/handlers/http/customerorder" "furtuna-be/internal/handlers/http/discovery" "furtuna-be/internal/middlewares" @@ -17,6 +19,8 @@ func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceMan serverRoutes := []HTTPHandlerRoutes{ discovery.NewHandler(serviceManager.DiscoverService), + customerauth.NewAuthHandler(serviceManager.AuthSvc, serviceManager.UserSvc), + customerorder.NewHandler(serviceManager.OrderSvc), } for _, handler := range serverRoutes { diff --git a/internal/services/discovery/discovery.go b/internal/services/discovery/discovery.go index 755b0c1..52c05dd 100644 --- a/internal/services/discovery/discovery.go +++ b/internal/services/discovery/discovery.go @@ -2,7 +2,9 @@ package discovery import ( "context" + "errors" "furtuna-be/config" + "gorm.io/gorm" "furtuna-be/internal/entity" "furtuna-be/internal/repository" @@ -15,14 +17,16 @@ const ( ) type DiscoveryService struct { - repo repository.SiteRepository - cfg config.Discovery + repo repository.SiteRepository + cfg config.Discovery + product repository.Product } -func NewDiscoveryService(repo repository.SiteRepository, cfg config.Discovery) *DiscoveryService { +func NewDiscoveryService(repo repository.SiteRepository, cfg config.Discovery, product repository.Product) *DiscoveryService { return &DiscoveryService{ - repo: repo, - cfg: cfg, + repo: repo, + cfg: cfg, + product: product, } } @@ -119,3 +123,43 @@ func (s *DiscoveryService) Search(ctx context.Context, search *entity.DiscoveryS return response, total, nil } + +func (s *DiscoveryService) GetByID(ctx context.Context, id int64) (*entity.Site, error) { + site, err := s.repo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + if site.Status == "Inactive" { + return nil, nil + } + + return site.ToSite(), nil +} + +func (s *DiscoveryService) GetProductsByID(ctx context.Context, id int64) ([]*entity.Product, error) { + site, err := s.repo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + if site.Status == "Inactive" { + return nil, nil + } + + product, err := s.product.GetProductsBySiteID(ctx, site.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + return product.ToProductList(), nil +} diff --git a/internal/services/order/order.go b/internal/services/order/order.go index a5f03b4..8d70620 100644 --- a/internal/services/order/order.go +++ b/internal/services/order/order.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + errors2 "furtuna-be/internal/common/errors" "furtuna-be/internal/common/logger" "furtuna-be/internal/common/mycontext" order2 "furtuna-be/internal/constants/order" @@ -81,6 +82,7 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque SiteID: ctx.GetSiteID(), CreatedBy: req.CreatedBy, OrderItems: []entity.OrderItem{}, + Source: req.Source, } for _, item := range req.OrderItems { @@ -191,6 +193,7 @@ func (s *OrderService) processNonCashPayment(ctx context.Context, order *entity. PaymentReferenceID: generator.GenerateUUIDV4(), TotalAmount: int64(order.Amount), OrderItems: order.OrderItems, + PaymentMethod: order.PaymentType, } paymentResponse, err := s.midtrans.CreatePayment(paymentRequest) @@ -358,3 +361,17 @@ func (s OrderService) SumAmount(ctx mycontext.Context, req entity.OrderSearch) ( return data, nil } + +func (s *OrderService) GetByID(ctx mycontext.Context, id int64) (*entity.Order, error) { + order, err := s.repo.FindByID(ctx, id) + if err != nil { + logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err)) + return nil, err + } + + if order.CreatedBy != ctx.RequestedBy() { + return nil, errors2.NewError(errors2.ErrorBadRequest.ErrorType(), "order not found") + } + + return order, nil +} diff --git a/internal/services/service.go b/internal/services/service.go index cde2232..d287714 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -58,7 +58,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) LicenseSvc: service.NewLicenseService(repo.License), Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx), Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction), - DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery), + DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery, repo.Product), } } @@ -119,6 +119,7 @@ type Order interface { SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.Order, error) GetDailySales(ctx mycontext.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error) GetPaymentDistribution(ctx mycontext.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) + GetByID(ctx mycontext.Context, id int64) (*entity.Order, error) } type OSSService interface { @@ -163,4 +164,6 @@ type Balance interface { type DiscoverService interface { Home(ctx context.Context, search *entity.DiscoverySearch) (*entity.DiscoverySearchResp, error) Search(ctx context.Context, search *entity.DiscoverySearch) (*entity.DiscoverySearchResp, int64, error) + GetByID(ctx context.Context, id int64) (*entity.Site, error) + GetProductsByID(ctx context.Context, id int64) ([]*entity.Product, error) }