package http import ( "enaklo-pos-be/internal/common/errors" order2 "enaklo-pos-be/internal/constants/order" "enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/handlers/request" "enaklo-pos-be/internal/handlers/response" "enaklo-pos-be/internal/services/v2/order" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" ) type Handler struct { service order.Service queryParser *request.QueryParser } func NewOrderHandler(service order.Service) *Handler { return &Handler{ service: service, queryParser: request.NewQueryParser(), } } 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.POST("/refund", jwt, h.Refund) route.POST("/partial-refund", jwt, h.PartialRefund) route.POST("/void", jwt, h.VoidOrder) route.POST("/split-bill", jwt, h.SplitBill) route.GET("/history", jwt, h.GetOrderHistory) route.GET("/refund-history", jwt, h.GetRefundHistory) route.GET("/payment-analysis", jwt, h.GetPaymentMethodAnalysis) route.GET("/revenue-overview", jwt, h.GetRevenueOverview) route.GET("/sales-by-category", jwt, h.GetSalesByCategory) route.GET("/popular-products", jwt, h.GetPopularProducts) route.GET("/detail/:id", jwt, h.GetByID) } type InquiryRequest struct { CustomerID *int64 `json:"customer_id"` CustomerName string `json:"customer_name" validate:"required_without=CustomerID"` CustomerEmail string `json:"customer_email"` CustomerPhoneNumber string `json:"customer_phone_number"` PaymentMethod string `json:"payment_method" validate:"required"` OrderItems []OrderItemRequest `json:"order_items" validate:"required,min=1,dive"` OrderType string `json:"order_type"` PaymentProvider string `json:"payment_provider"` TableNumber string `json:"table_number"` CashierSessionID int64 `json:"cashier_session_id"` } func (o *InquiryRequest) GetPaymentProvider() string { if o.PaymentMethod == "CASH" { return "CASH" } return o.PaymentProvider } type OrderItemRequest struct { ProductID int64 `json:"product_id" validate:"required"` Quantity int `json:"quantity" validate:"required,min=1"` Notes string `json:"notes"` } type ExecuteRequest struct { PaymentMethod string `json:"payment_method" validate:"required"` PaymentProvider string `json:"payment_provider"` InProgressOrderID int64 `json:"in_progress_order_id"` Token string `json:"token"` } type RefundRequest struct { OrderID int64 `json:"order_id" validate:"required"` Reason string `json:"reason" validate:"required"` } type PartialRefundRequest struct { OrderID int64 `json:"order_id" validate:"required"` Reason string `json:"reason" validate:"required"` Items []PartialRefundItemRequest `json:"items" validate:"required,min=1,dive"` } type PartialRefundItemRequest struct { OrderItemID int64 `json:"order_item_id" validate:"required"` Quantity int `json:"quantity" validate:"required,min=1"` } type VoidOrderRequest struct { OrderID int64 `json:"order_id" validate:"required"` Reason string `json:"reason" validate:"required"` Type string `json:"type" validate:"required,oneof=ALL ITEM"` Items []VoidItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"` } type VoidItemRequest struct { OrderItemID int64 `json:"order_item_id" validate:"required"` Quantity int `json:"quantity" validate:"required,min=1"` } type SplitBillRequest struct { OrderID int64 `json:"order_id" validate:"required"` Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"` Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"` Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"` } type SplitBillItemRequest struct { OrderItemID int64 `json:"order_item_id" validate:"required"` Quantity int `json:"quantity" validate:"required,min=1"` } type RefundResponse struct { OrderID int64 `json:"order_id"` Status string `json:"status"` RefundAmount float64 `json:"refund_amount"` Reason string `json:"reason"` RefundedAt string `json:"refunded_at"` CustomerName string `json:"customer_name"` PaymentType string `json:"payment_type"` } type RefundHistoryResponse struct { OrderID int64 `json:"order_id"` CustomerName string `json:"customer_name"` CustomerID *int64 `json:"customer_id"` IsMember bool `json:"is_member"` Status string `json:"status"` Amount float64 `json:"amount"` Total float64 `json:"total"` PaymentType string `json:"payment_type"` TableNumber string `json:"table_number"` OrderType string `json:"order_type"` CreatedAt string `json:"created_at"` RefundedAt string `json:"refunded_at"` Tax float64 `json:"tax"` } type PartialRefundResponse struct { OrderID int64 `json:"order_id"` Status string `json:"status"` RefundedAmount float64 `json:"refunded_amount"` RemainingAmount float64 `json:"remaining_amount"` Reason string `json:"reason"` RefundedAt string `json:"refunded_at"` CustomerName string `json:"customer_name"` PaymentType string `json:"payment_type"` RefundedItems []RefundedItemResponse `json:"refunded_items"` } type RefundedItemResponse struct { OrderItemID int64 `json:"order_item_id"` ItemName string `json:"item_name"` Quantity int `json:"quantity"` UnitPrice float64 `json:"unit_price"` TotalPrice float64 `json:"total_price"` } type VoidOrderResponse struct { OrderID int64 `json:"order_id"` Status string `json:"status"` Reason string `json:"reason"` VoidedAt string `json:"voided_at"` CustomerName string `json:"customer_name"` VoidedItems []VoidedItemResponse `json:"voided_items,omitempty"` } type VoidedItemResponse struct { OrderItemID int64 `json:"order_item_id"` ItemName string `json:"item_name"` Quantity int `json:"quantity"` UnitPrice float64 `json:"unit_price"` TotalPrice float64 `json:"total_price"` } type SplitBillResponse struct { OriginalOrderID int64 `json:"original_order_id"` SplitOrders []SplitOrderResponse `json:"split_orders"` SplitAt string `json:"split_at"` } type SplitOrderResponse struct { OrderID int64 `json:"order_id"` CustomerName string `json:"customer_name"` CustomerID *int64 `json:"customer_id"` Amount float64 `json:"amount"` Total float64 `json:"total"` Tax float64 `json:"tax"` Status string `json:"status"` Items []response.OrderItemResponse `json:"items"` } func (h *Handler) Inquiry(c *gin.Context) { ctx := request.GetMyContext(c) userID := ctx.RequestedBy() partnerID := ctx.GetPartnerID() var req InquiryRequest 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 } orderItems := make([]entity.OrderItemRequest, len(req.OrderItems)) for i, item := range req.OrderItems { orderItems[i] = entity.OrderItemRequest{ ProductID: item.ProductID, Quantity: item.Quantity, Notes: item.Notes, } } orderReq := &entity.OrderRequest{ Source: "POS", CreatedBy: userID, PartnerID: *partnerID, PaymentMethod: req.PaymentMethod, OrderItems: orderItems, CustomerID: req.CustomerID, CustomerName: req.CustomerName, CustomerEmail: req.CustomerEmail, CustomerPhoneNumber: req.CustomerPhoneNumber, OrderType: req.OrderType, PaymentProvider: req.GetPaymentProvider(), TableNumber: req.TableNumber, CashierSessionID: req.CashierSessionID, } result, err := h.service.CreateOrderInquiry(ctx, orderReq) if err != nil { response.ErrorWrapper(c, err) return } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: response.MapToInquiryResponse(result), }) } func (h *Handler) Execute(c *gin.Context) { ctx := request.GetMyContext(c) var req ExecuteRequest 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 } result, err := h.service.ExecuteOrderInquiry(ctx, req.Token, req.PaymentMethod, req.PaymentProvider, req.InProgressOrderID) if err != nil { response.ErrorWrapper(c, err) return } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: response.MapToOrderResponse(result), }) } func (h *Handler) Refund(c *gin.Context) { ctx := request.GetMyContext(c) var req RefundRequest 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 } err := h.service.RefundRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason) if err != nil { response.ErrorWrapper(c, err) return } order, err := h.service.GetOrderByID(ctx, req.OrderID) if err != nil { c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Message: "Refund processed successfully", }) return } refundResponse := RefundResponse{ OrderID: order.ID, Status: order.Status, RefundAmount: order.Total, Reason: req.Reason, RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), CustomerName: order.CustomerName, PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider), } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: refundResponse, }) } func (h *Handler) GetOrderHistory(c *gin.Context) { ctx := request.GetMyContext(c) searchReq, err := h.queryParser.ParseSearchRequest(c) if err != nil { response.ErrorWrapper(c, err) return } orders, total, err := h.service.GetOrderHistory(ctx, *searchReq) if err != nil { response.ErrorWrapper(c, err) return } responseData := response.MapOrderHistoryResponse(orders) pagingMeta := response.NewPaginationHelper().BuildPagingMeta(searchReq.Offset, searchReq.Limit, total) c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: responseData, PagingMeta: pagingMeta, }) } func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) { ctx := request.GetMyContext(c) partnerID := ctx.GetPartnerID() // Parse query parameters limitStr := c.Query("limit") offsetStr := c.Query("offset") status := c.Query("status") startDateStr := c.Query("start_date") endDateStr := c.Query("end_date") searchReq := entity.SearchRequest{} limit := 10 if limitStr != "" { parsedLimit, err := strconv.Atoi(limitStr) if err == nil && parsedLimit > 0 { limit = parsedLimit } } if limit > 20 { limit = 20 } searchReq.Limit = limit offset := 0 if offsetStr != "" { parsedOffset, err := strconv.Atoi(offsetStr) if err == nil && parsedOffset >= 0 { offset = parsedOffset } } searchReq.Offset = offset if status != "" { searchReq.Status = status } if startDateStr != "" { startDate, err := time.Parse(time.RFC3339, startDateStr) if err == nil { searchReq.Start = startDate } } if endDateStr != "" { endDate, err := time.Parse(time.RFC3339, endDateStr) if err == nil { searchReq.End = endDate } } paymentAnalysis, err := h.service.GetOrderPaymentAnalysis(ctx, *partnerID, searchReq) if err != nil { response.ErrorWrapper(c, err) return } paymentBreakdown := make([]PaymentMethodBreakdown, len(paymentAnalysis.PaymentMethodBreakdown)) for i, bd := range paymentAnalysis.PaymentMethodBreakdown { paymentBreakdown[i] = PaymentMethodBreakdown{ PaymentMethod: response.NewPaymentFormatter().Format(bd.PaymentType, bd.PaymentProvider), TotalTransactions: bd.TotalTransactions, TotalAmount: bd.TotalAmount, } } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: PaymentMethodAnalysisResponse{ PaymentMethodBreakdown: paymentBreakdown, TotalAmount: paymentAnalysis.TotalAmount, TotalTransactions: paymentAnalysis.TotalTransactions, }, }) } type PaymentMethodBreakdown struct { PaymentMethod string `json:"payment_method"` TotalTransactions int64 `json:"total_transactions"` TotalAmount float64 `json:"total_amount"` AverageTransactionAmount float64 `json:"average_transaction_amount"` Percentage float64 `json:"percentage"` } type PaymentMethodAnalysisResponse struct { PaymentMethodBreakdown []PaymentMethodBreakdown `json:"payment_method_breakdown"` TotalAmount float64 `json:"total_amount"` TotalTransactions int64 `json:"total_transactions"` MostUsedPaymentMethod string `json:"most_used_payment_method"` HighestRevenueMethod string `json:"highest_revenue_method"` } func (h *Handler) GetRevenueOverview(c *gin.Context) { ctx := request.GetMyContext(c) partnerID := ctx.GetPartnerID() granularity := c.Query("period") year := time.Now().Year() if granularity != "m" && granularity != "w" && granularity != "d" { granularity = "m" } revenueOverview, err := h.service.GetRevenueOverview( ctx, *partnerID, year, granularity, order2.Paid.String(), ) if err != nil { response.ErrorWrapper(c, err) return } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: revenueOverview, }) } func (h *Handler) GetSalesByCategory(c *gin.Context) { ctx := request.GetMyContext(c) partnerID := ctx.GetPartnerID() period := c.Query("period") status := order2.Paid.String() if period != "d" && period != "w" && period != "m" { period = "d" } salesByCategory, err := h.service.GetSalesByCategory( ctx, *partnerID, period, status, ) if err != nil { response.ErrorWrapper(c, err) return } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: salesByCategory, }) } func (h *Handler) GetPopularProducts(c *gin.Context) { ctx := request.GetMyContext(c) partnerID := ctx.GetPartnerID() period := c.Query("period") status := order2.Paid.String() sortBy := c.Query("sort_by") limit := 1000 if period != "d" && period != "w" && period != "m" { period = "d" } popularProducts, err := h.service.GetPopularProducts( ctx, *partnerID, period, status, limit, sortBy, ) if err != nil { response.ErrorWrapper(c, err) return } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: popularProducts, }) } func (h *Handler) GetRefundHistory(c *gin.Context) { ctx := request.GetMyContext(c) limitStr := c.Query("limit") offsetStr := c.Query("offset") startDateStr := c.Query("start_date") endDateStr := c.Query("end_date") searchReq := entity.SearchRequest{} limit := 20 if limitStr != "" { parsedLimit, err := strconv.Atoi(limitStr) if err == nil && parsedLimit > 0 { limit = parsedLimit } } if limit > 100 { limit = 100 } searchReq.Limit = limit offset := 0 if offsetStr != "" { parsedOffset, err := strconv.Atoi(offsetStr) if err == nil && parsedOffset >= 0 { offset = parsedOffset } } searchReq.Offset = offset searchReq.Status = "REFUNDED" if startDateStr != "" { startDate, err := time.Parse(time.RFC3339, startDateStr) if err == nil { searchReq.Start = startDate } } if endDateStr != "" { endDate, err := time.Parse(time.RFC3339, endDateStr) if err == nil { searchReq.End = endDate } } orders, total, err := h.service.GetOrderHistory(ctx, searchReq) if err != nil { response.ErrorWrapper(c, err) return } responseData := []RefundHistoryResponse{} for _, order := range orders { responseData = append(responseData, RefundHistoryResponse{ OrderID: order.ID, CustomerName: order.CustomerName, CustomerID: order.CustomerID, IsMember: order.IsMemberOrder(), Status: order.Status, Amount: order.Amount, Total: order.Total, PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider), TableNumber: order.TableNumber, OrderType: order.OrderType, CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"), RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), Tax: order.Tax, }) } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: responseData, PagingMeta: &response.PagingMeta{ Page: offset + 1, Total: int64(total), Limit: limit, }, }) } func (h *Handler) PartialRefund(c *gin.Context) { ctx := request.GetMyContext(c) var req PartialRefundRequest 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 } items := make([]entity.PartialRefundItem, len(req.Items)) for i, item := range req.Items { items[i] = entity.PartialRefundItem{ OrderItemID: item.OrderItemID, Quantity: item.Quantity, } } err := h.service.PartialRefundRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason, items) if err != nil { response.ErrorWrapper(c, err) return } // Get updated order to return details order, err := h.service.GetOrderByID(ctx, req.OrderID) if err != nil { c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Message: "Partial refund processed successfully", }) return } refundedAmount := 0.0 var refundedItems []RefundedItemResponse for _, reqItem := range req.Items { for _, orderItem := range order.OrderItems { if orderItem.ID == reqItem.OrderItemID { itemTotal := orderItem.Price * float64(reqItem.Quantity) refundedAmount += itemTotal refundedItems = append(refundedItems, RefundedItemResponse{ OrderItemID: orderItem.ID, ItemName: orderItem.ItemName, Quantity: reqItem.Quantity, UnitPrice: orderItem.Price, TotalPrice: itemTotal, }) break } } } partialRefundResponse := PartialRefundResponse{ OrderID: order.ID, Status: order.Status, RefundedAmount: refundedAmount, RemainingAmount: order.Total, Reason: req.Reason, RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), CustomerName: order.CustomerName, PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider), RefundedItems: refundedItems, } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: partialRefundResponse, }) } func (h *Handler) VoidOrder(c *gin.Context) { ctx := request.GetMyContext(c) var req VoidOrderRequest 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 } var items []entity.VoidItem if req.Type == "ITEM" { items = make([]entity.VoidItem, len(req.Items)) for i, item := range req.Items { items[i] = entity.VoidItem{ OrderItemID: item.OrderItemID, Quantity: item.Quantity, } } } err := h.service.VoidOrderRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason, req.Type, items) if err != nil { response.ErrorWrapper(c, err) return } // Get updated order to return details order, err := h.service.GetOrderByID(ctx, req.OrderID) if err != nil { c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Message: "Order voided successfully", }) return } var voidedItems []VoidedItemResponse if req.Type == "ITEM" { for _, reqItem := range req.Items { for _, orderItem := range order.OrderItems { if orderItem.ID == reqItem.OrderItemID { itemTotal := orderItem.Price * float64(reqItem.Quantity) voidedItems = append(voidedItems, VoidedItemResponse{ OrderItemID: orderItem.ID, ItemName: orderItem.ItemName, Quantity: reqItem.Quantity, UnitPrice: orderItem.Price, TotalPrice: itemTotal, }) break } } } } voidOrderResponse := VoidOrderResponse{ OrderID: order.ID, Status: order.Status, Reason: req.Reason, VoidedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), CustomerName: order.CustomerName, VoidedItems: voidedItems, } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: voidOrderResponse, }) } func (h *Handler) SplitBill(c *gin.Context) { ctx := request.GetMyContext(c) var req SplitBillRequest 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 } var items []entity.SplitBillItem if req.Type == "ITEM" { items = make([]entity.SplitBillItem, len(req.Items)) for i, item := range req.Items { items[i] = entity.SplitBillItem{ OrderItemID: item.OrderItemID, Quantity: item.Quantity, } } } splitOrder, err := h.service.SplitBillRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Type, items, req.Amount) if err != nil { response.ErrorWrapper(c, err) return } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}), }) } func (h *Handler) GetByID(c *gin.Context) { ctx := request.GetMyContext(c) partnerID := ctx.GetPartnerID() orderID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.ErrorWrapper(c, errors.ErrorBadRequest) return } order, err := h.service.GetOrderByIDAndPartnerID(ctx, orderID, *partnerID) if err != nil { response.ErrorWrapper(c, err) return } c.JSON(http.StatusOK, response.BaseResponse{ Success: true, Status: http.StatusOK, Data: response.MapToOrderResponse(&entity.OrderResponse{Order: order}), }) }