diff --git a/internal/constants/order/order.go b/internal/constants/order/order.go index 27c02ba..3bc95ec 100644 --- a/internal/constants/order/order.go +++ b/internal/constants/order/order.go @@ -3,9 +3,10 @@ package order type OrderStatus string const ( - New OrderStatus = "NEW" - Paid OrderStatus = "PAID" - Cancel OrderStatus = "CANCEL" + New OrderStatus = "NEW" + Paid OrderStatus = "PAID" + Cancel OrderStatus = "CANCEL" + Pending OrderStatus = "PENDING" ) func (b OrderStatus) toString() string { diff --git a/internal/entity/order.go b/internal/entity/order.go index 4661286..3f0dce9 100644 --- a/internal/entity/order.go +++ b/internal/entity/order.go @@ -10,7 +10,7 @@ type Order struct { Status string `gorm:"type:varchar;column:status"` Amount float64 `gorm:"type:numeric;not null;column:amount"` Total float64 `gorm:"type:numeric;not null;column:total"` - Fee float64 `gorm:"type:numeric;not null;column:fee"` + Tax float64 `gorm:"type:numeric;not null;column:tax"` CustomerID *int64 CustomerName string InquiryID *string @@ -27,7 +27,7 @@ type Order struct { Source string `gorm:"type:varchar;column:source"` OrderType string `gorm:"type:varchar;column:order_type"` TableNumber string - InProgressOrderID string + InProgressOrderID int64 } type OrderDB struct { @@ -86,6 +86,7 @@ type OrderItem struct { 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"` } func (OrderItem) TableName() string { @@ -105,6 +106,7 @@ type OrderRequest struct { TableNumber string PaymentProvider string OrderType string + ID int64 } type OrderItemRequest struct { diff --git a/internal/entity/order_inquiry.go b/internal/entity/order_inquiry.go index 0ba0836..1af1109 100644 --- a/internal/entity/order_inquiry.go +++ b/internal/entity/order_inquiry.go @@ -14,7 +14,7 @@ type OrderInquiry struct { CustomerEmail string `json:"customer_email"` Status string `json:"status"` Amount float64 `json:"amount"` - Fee float64 `json:"fee"` + Tax float64 `json:"tax"` Total float64 `json:"total"` PaymentType string `json:"payment_type"` Source string `json:"source"` @@ -30,7 +30,7 @@ type OrderInquiry struct { type OrderCalculation struct { Subtotal float64 `json:"subtotal"` - Fee float64 `json:"fee"` + Tax float64 `json:"tax"` Total float64 `json:"total"` } @@ -43,7 +43,7 @@ func NewOrderInquiry( partnerID int64, customerID int64, amount float64, - fee float64, + tax float64, total float64, paymentType string, source string, @@ -60,7 +60,7 @@ func NewOrderInquiry( PartnerID: partnerID, Status: "PENDING", Amount: amount, - Fee: fee, + Tax: tax, Total: total, PaymentType: paymentType, CustomerID: customerID, @@ -83,6 +83,7 @@ func (oi *OrderInquiry) AddOrderItem(item OrderItemRequest, product *Product) { ItemID: item.ProductID, ItemType: product.Type, Price: product.Price, + ItemName: product.Name, Quantity: item.Quantity, CreatedBy: oi.CreatedBy, Product: product, @@ -98,7 +99,7 @@ func (i *OrderInquiry) ToOrder(paymentMethod, paymentProvider string) *Order { InquiryID: &i.ID, Status: constants.StatusPaid, Amount: i.Amount, - Fee: i.Fee, + Tax: i.Tax, Total: i.Total, PaymentType: paymentMethod, PaymentProvider: paymentProvider, @@ -107,6 +108,8 @@ func (i *OrderInquiry) ToOrder(paymentMethod, paymentProvider string) *Order { CreatedAt: now, OrderItems: make([]OrderItem, len(i.OrderItems)), OrderType: i.OrderType, + CustomerName: i.CustomerName, + TableNumber: i.TableNumber, } for idx, item := range i.OrderItems { @@ -114,6 +117,7 @@ func (i *OrderInquiry) ToOrder(paymentMethod, paymentProvider string) *Order { ItemID: item.ItemID, ItemType: item.ItemType, Price: item.Price, + ItemName: item.ItemName, Quantity: item.Quantity, CreatedBy: i.CreatedBy, CreatedAt: now, diff --git a/internal/entity/partner_setting.go b/internal/entity/partner_setting.go new file mode 100644 index 0000000..8e9e6bd --- /dev/null +++ b/internal/entity/partner_setting.go @@ -0,0 +1,56 @@ +package entity + +import ( + "time" +) + +type PartnerSettings struct { + PartnerID int64 `json:"partner_id"` + TaxEnabled bool `json:"tax_enabled"` + TaxPercentage float64 `json:"tax_percentage"` + InvoicePrefix string `json:"invoice_prefix"` + BusinessHours string `json:"business_hours"` + LogoURL string `json:"logo_url"` + ThemeColor string `json:"theme_color"` + ReceiptFooterText string `json:"receipt_footer_text"` + ReceiptHeaderText string `json:"receipt_header_text"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type PartnerPaymentMethod struct { + ID int64 `json:"id"` + PartnerID int64 `json:"partner_id"` + PaymentMethod string `json:"payment_method"` + IsEnabled bool `json:"is_enabled"` + DisplayName string `json:"display_name"` + DisplayOrder int `json:"display_order"` + AdditionalInfo string `json:"additional_info"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type PartnerFeatureFlag struct { + ID int64 `json:"id"` + PartnerID int64 `json:"partner_id"` + FeatureKey string `json:"feature_key"` + IsEnabled bool `json:"is_enabled"` + Config string `json:"config"` // JSON string + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type BusinessHoursSetting struct { + Monday DayHours `json:"monday"` + Tuesday DayHours `json:"tuesday"` + Wednesday DayHours `json:"wednesday"` + Thursday DayHours `json:"thursday"` + Friday DayHours `json:"friday"` + Saturday DayHours `json:"saturday"` + Sunday DayHours `json:"sunday"` +} + +type DayHours struct { + Open string `json:"open"` // Format: "HH:MM" + Close string `json:"close"` // Format: "HH:MM" +} diff --git a/internal/entity/product.go b/internal/entity/product.go index 6febc12..6ba1c3e 100644 --- a/internal/entity/product.go +++ b/internal/entity/product.go @@ -128,3 +128,39 @@ type ProductDetails struct { Products map[int64]*Product // Map for quick lookups by ID PartnerID int64 // Common site ID for all products } + +type PaymentMethodBreakdown struct { + PaymentType string `json:"payment_type"` + PaymentProvider string `json:"payment_provider"` + TotalTransactions int64 `json:"total_transactions"` + TotalAmount float64 `json:"total_amount"` +} + +type OrderPaymentAnalysis struct { + TotalTransactions int64 `json:"total"` + TotalAmount float64 `json:"total_amount"` + PaymentMethodBreakdown []PaymentMethodBreakdown `json:"payment_method_breakdown"` +} + +type RevenueOverviewItem struct { + Period string `json:"period"` + TotalAmount float64 `json:"total_amount"` + OrderCount int64 `json:"order_count"` +} + +type SalesByCategoryItem struct { + Category string `json:"category"` + TotalAmount float64 `json:"total_amount"` + TotalQuantity int64 `json:"total_quantity"` + Percentage float64 `json:"percentage"` +} + +type PopularProductItem struct { + ProductID int64 `json:"product_id"` + ProductName string `json:"product_name"` + Category string `json:"category"` + TotalSales int64 `json:"total_sales"` + TotalRevenue float64 `json:"total_revenue"` + AveragePrice float64 `json:"average_price"` + Percentage float64 `json:"percentage"` +} diff --git a/internal/entity/search.go b/internal/entity/search.go new file mode 100644 index 0000000..b0a12f8 --- /dev/null +++ b/internal/entity/search.go @@ -0,0 +1,32 @@ +package entity + +import "time" + +type SearchRequest struct { + Status string // Filter by order status (e.g., "COMPLETED", "PENDING", etc.) + Start time.Time // Start date for filtering orders + End time.Time // End date for filtering orders + Limit int // Maximum number of records to return + Offset int // Number of records to skip for pagination +} + +type RevenueOverviewRequest struct { + PartnerID int64 + Year int + Granularity string // "monthly" or "weekly" + Status string +} + +type SalesByCategoryRequest struct { + PartnerID int64 + Period string // "d" (daily), "w" (weekly), "m" (monthly) + Status string +} + +type PopularProductsRequest struct { + PartnerID int64 + Period string // "d" (daily), "w" (weekly), "m" (monthly) + Status string + Limit int + SortBy string // "sales" or "revenue" +} diff --git a/internal/handlers/http/customerorder/order.go b/internal/handlers/http/customerorder/order.go index 46d4826..f3afba4 100644 --- a/internal/handlers/http/customerorder/order.go +++ b/internal/handlers/http/customerorder/order.go @@ -109,7 +109,7 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse, req requ PaymentType: order.PaymentType, CreatedAt: order.CreatedAt, OrderItems: orderItems, - Fee: order.Fee, + Tax: order.Tax, Total: order.Total, } } @@ -249,7 +249,7 @@ func (h *Handler) toOrderDetail(order *entity.Order) *response.OrderDetail { PaymentLink: paymentLink, PaymentToken: paymentToken, SiteName: siteName, - Fee: order.Fee, + Fee: order.Tax, } orderDetail.OrderItems = make([]response.OrderDetailItem, len(order.OrderItems)) diff --git a/internal/handlers/http/inprogress_order.go b/internal/handlers/http/inprogress_order.go index 7d54c2a..5037945 100644 --- a/internal/handlers/http/inprogress_order.go +++ b/internal/handlers/http/inprogress_order.go @@ -38,7 +38,7 @@ type CreateInProgressOrderRequest struct { OrderType string `json:"order_type"` PaymentProvider string `json:"payment_provider"` TableNumber string `json:"table_number"` - InProgressOrderID string `json:"in_progress_order_id"` + InProgressOrderID int64 `json:"in_progress_order_id"` } type InProgressOrderItemRequest struct { @@ -72,15 +72,15 @@ func (h *InProgressOrderHandler) Save(c *gin.Context) { return } - orderItems := make([]entity.InProgressOrderItem, len(req.OrderItems)) + orderItems := make([]entity.OrderItemRequest, len(req.OrderItems)) for i, item := range req.OrderItems { - orderItems[i] = entity.InProgressOrderItem{ - ItemID: item.ProductID, - Quantity: item.Quantity, + orderItems[i] = entity.OrderItemRequest{ + ProductID: item.ProductID, + Quantity: item.Quantity, } } - order := &entity.InProgressOrder{ + order := &entity.OrderRequest{ PartnerID: *partnerID, CustomerID: req.CustomerID, CustomerName: req.CustomerName, @@ -89,6 +89,7 @@ func (h *InProgressOrderHandler) Save(c *gin.Context) { TableNumber: req.TableNumber, OrderType: req.OrderType, ID: req.InProgressOrderID, + Source: "POS", } _, err := h.service.Save(ctx, order) @@ -103,7 +104,7 @@ func (h *InProgressOrderHandler) Save(c *gin.Context) { }) } -func mapToInProgressOrderResponse(order *entity.InProgressOrder) map[string]interface{} { +func mapToInProgressOrderResponse(order *entity.Order) map[string]interface{} { orderItems := make([]map[string]interface{}, len(order.OrderItems)) for i, item := range order.OrderItems { orderItems[i] = map[string]interface{}{ diff --git a/internal/handlers/http/order.go b/internal/handlers/http/order.go index df48f1c..95c0fcd 100644 --- a/internal/handlers/http/order.go +++ b/internal/handlers/http/order.go @@ -2,6 +2,7 @@ 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" @@ -9,6 +10,8 @@ import ( "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "net/http" + "strconv" + "time" ) type Handler struct { @@ -26,6 +29,12 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { route.POST("/inquiry", jwt, h.Inquiry) route.POST("/execute", jwt, h.Execute) + route.GET("/history", jwt, h.GetOrderHistory) + 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) + } type InquiryRequest struct { @@ -56,7 +65,7 @@ type OrderItemRequest struct { type ExecuteRequest struct { PaymentMethod string `json:"payment_method" validate:"required"` PaymentProvider string `json:"payment_provider"` - InProgressOrderID string `json:"in_progress_order_id"` + InProgressOrderID int64 `json:"in_progress_order_id"` Token string `json:"token"` } @@ -140,3 +149,307 @@ func (h *Handler) Execute(c *gin.Context) { Data: response.MapToOrderResponse(result), }) } + +func (h *Handler) GetOrderHistory(c *gin.Context) { + ctx := request.GetMyContext(c) + partnerID := ctx.GetPartnerID() + + limitStr := c.Query("limit") + offsetStr := c.Query("offset") + status := c.Query("status") + startDateStr := c.Query("start_date") + endDateStr := c.Query("end_date") + + // Build search request + searchReq := entity.SearchRequest{} + + // Set status if provided + if status != "" { + searchReq.Status = status + } + + // Parse and set limit + limit := 10 + if limitStr != "" { + parsedLimit, err := strconv.Atoi(limitStr) + if err == nil && parsedLimit > 0 { + limit = parsedLimit + } + } + if limit > 20 { + limit = 20 + } + searchReq.Limit = limit + + // Parse and set offset + offset := 0 + if offsetStr != "" { + parsedOffset, err := strconv.Atoi(offsetStr) + if err == nil && parsedOffset >= 0 { + offset = parsedOffset + } + } + searchReq.Offset = offset + + if startDateStr != "" { + startDate, err := time.Parse(time.RFC3339, startDateStr) + if err == nil { + searchReq.Start = startDate + } + } + + // Parse end date if provided + if endDateStr != "" { + endDate, err := time.Parse(time.RFC3339, endDateStr) + if err == nil { + searchReq.End = endDate + } + } + + orders, total, err := h.service.GetOrderHistory(ctx, *partnerID, searchReq) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + responseData := []response.OrderHistoryResponse{} + for _, order := range orders { + var orderItems []response.OrderItemResponse + for _, item := range order.OrderItems { + orderItems = append(orderItems, response.OrderItemResponse{ + ProductID: item.ItemID, + ProductName: item.ItemName, + Price: item.Price, + Quantity: item.Quantity, + Subtotal: item.Price * float64(item.Quantity), + }) + } + + responseData = append(responseData, response.OrderHistoryResponse{ + ID: order.ID, + CustomerName: order.CustomerName, + Status: order.Status, + Amount: order.Amount, + Total: order.Total, + PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider), + TableNumber: order.TableNumber, + OrderType: order.OrderType, + OrderItems: orderItems, + CreatedAt: order.CreatedAt.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) formatPayment(payment, provider string) string { + if payment == "CASH" { + return payment + } + + return payment + " " + provider +} + +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: h.formatPayment(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() + limitStr := c.Query("limit") + sortBy := c.Query("sort_by") + + limit, err := strconv.Atoi(limitStr) + if err != nil { + limit = 10 // default limit + } + + 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, + }) +} diff --git a/internal/handlers/http/order/order.go b/internal/handlers/http/order/order.go index f966e4d..e5bc974 100644 --- a/internal/handlers/http/order/order.go +++ b/internal/handlers/http/order/order.go @@ -204,7 +204,7 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response Status: order.Status, Amount: order.Amount, Total: order.Total, - Fee: order.Fee, + Tax: order.Tax, PaymentType: order.PaymentType, CreatedAt: order.CreatedAt, OrderItems: orderItems, diff --git a/internal/handlers/http/partner/partner.go b/internal/handlers/http/partner/partner.go index 7d7f40f..002e9a5 100644 --- a/internal/handlers/http/partner/partner.go +++ b/internal/handlers/http/partner/partner.go @@ -28,6 +28,7 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { route.PUT("/:id", jwt, isSuperAdmin, h.Update) route.GET("/:id", jwt, isSuperAdmin, h.GetByID) route.DELETE("/:id", jwt, isSuperAdmin, h.Delete) + route.PUT("/update", jwt, h.UpdateMyStore) } func NewHandler(service services.Partner) *Handler { @@ -268,3 +269,27 @@ func (h *Handler) toPartnerResponseList(resp []*entity.Partner, total int64, req Offset: req.Offset, } } + +func (h *Handler) UpdateMyStore(c *gin.Context) { + ctx := request.GetMyContext(c) + + PartnerID := ctx.GetPartnerID() + + var req request.Partner + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + updatedPartner, err := h.service.Update(ctx, req.ToEntityUpdate(*PartnerID)) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: h.toPartnerResponse(updatedPartner), + }) +} diff --git a/internal/handlers/response/order.go b/internal/handlers/response/order.go index 05c89e2..baa04ad 100644 --- a/internal/handlers/response/order.go +++ b/internal/handlers/response/order.go @@ -88,7 +88,7 @@ type CreateOrderResponse struct { Status string `json:"status"` Amount float64 `json:"amount"` Total float64 `json:"total"` - Fee float64 `json:"fee"` + Tax float64 `json:"tax"` PaymentType string `json:"payment_type"` CreatedAt time.Time `json:"created_at"` OrderItems []CreateOrderItemResponse `json:"order_items"` @@ -187,3 +187,17 @@ type OrderDetailItem struct { UnitPrice float64 `json:"unit_price"` // Price per unit TotalPrice float64 `json:"total_price"` // Total price for this item (Quantity * UnitPrice) } + +type OrderHistoryResponse struct { + ID int64 `json:"id"` + CustomerName string `json:"customer_name"` + 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"` + OrderItems []OrderItemResponse `json:"order_items"` + CreatedAt string `json:"created_at"` + Tax float64 `json:"tax"` +} diff --git a/internal/handlers/response/order_inquiry.go b/internal/handlers/response/order_inquiry.go index d12bacd..506102d 100644 --- a/internal/handlers/response/order_inquiry.go +++ b/internal/handlers/response/order_inquiry.go @@ -9,7 +9,7 @@ type OrderInquiryResponse struct { ID string `json:"id"` Status string `json:"status"` Amount float64 `json:"amount"` - Fee float64 `json:"fee"` + Tax float64 `json:"tax"` Total float64 `json:"total"` CustomerID int64 `json:"customer_id"` PaymentType string `json:"payment_type"` @@ -54,7 +54,7 @@ func MapToInquiryResponse(result *entity.OrderInquiryResponse) OrderInquiryRespo ID: result.OrderInquiry.ID, Status: result.OrderInquiry.Status, Amount: result.OrderInquiry.Amount, - Fee: result.OrderInquiry.Fee, + Tax: result.OrderInquiry.Tax, Total: result.OrderInquiry.Total, CustomerID: result.OrderInquiry.CustomerID, PaymentType: result.OrderInquiry.PaymentType, @@ -74,7 +74,7 @@ type OrderResponse struct { ID int64 `json:"id"` Status string `json:"status"` Amount float64 `json:"amount"` - Fee float64 `json:"fee"` + Tax float64 `json:"tax"` Total float64 `json:"total"` CustomerName string `json:"customer_name,omitempty"` PaymentType string `json:"payment_type"` @@ -89,7 +89,7 @@ func MapToOrderResponse(result *entity.OrderResponse) OrderResponse { ID: result.Order.ID, Status: result.Order.Status, Amount: result.Order.Amount, - Fee: result.Order.Fee, + Tax: result.Order.Tax, Total: result.Order.Total, PaymentType: result.Order.PaymentType, CreatedAt: result.Order.CreatedAt, diff --git a/internal/handlers/response/paging.gp.go b/internal/handlers/response/paging.gp.go index 2ecbff0..dc5d0e5 100644 --- a/internal/handlers/response/paging.gp.go +++ b/internal/handlers/response/paging.gp.go @@ -3,5 +3,5 @@ package response type PagingMeta struct { Page int `json:"page"` Limit int `json:"limit"` - Total int64 `json:"total_data"` + Total int64 `json:"total"` } diff --git a/internal/repository/In_progress_orde_repo.go b/internal/repository/In_progress_orde_repo.go index 1d35f62..6ec8aac 100644 --- a/internal/repository/In_progress_orde_repo.go +++ b/internal/repository/In_progress_orde_repo.go @@ -2,7 +2,6 @@ package repository import ( "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants" "enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/repository/models" "github.com/pkg/errors" @@ -152,7 +151,7 @@ func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, pa func (r *inprogressOrderRepository) toInProgressOrderDBModel(order *entity.InProgressOrder) models.InProgressOrderDB { now := time2.Now() return models.InProgressOrderDB{ - ID: constants.GenerateUUID(), + ID: order.ID, PartnerID: order.PartnerID, CustomerID: order.CustomerID, CustomerName: order.CustomerName, @@ -207,7 +206,7 @@ func (r *inprogressOrderRepository) toOrderInquiryDBModel(inquiry *entity.OrderI CustomerID: &inquiry.CustomerID, Status: inquiry.Status, Amount: inquiry.Amount, - Fee: inquiry.Fee, + Tax: inquiry.Tax, Total: inquiry.Total, PaymentType: inquiry.PaymentType, Source: inquiry.Source, @@ -230,7 +229,7 @@ func (r *inprogressOrderRepository) toDomainOrderInquiryModel(dbModel *models.Or PartnerID: dbModel.PartnerID, Status: dbModel.Status, Amount: dbModel.Amount, - Fee: dbModel.Fee, + Tax: dbModel.Tax, Total: dbModel.Total, PaymentType: dbModel.PaymentType, Source: dbModel.Source, diff --git a/internal/repository/models/order.go b/internal/repository/models/order.go index 15535c6..086e656 100644 --- a/internal/repository/models/order.go +++ b/internal/repository/models/order.go @@ -5,20 +5,24 @@ import ( ) type OrderDB struct { - ID int64 `gorm:"primaryKey;column:id"` - PartnerID int64 `gorm:"column:partner_id"` - CustomerID *int64 `gorm:"column:customer_id"` - InquiryID *string `gorm:"column:inquiry_id"` - Status string `gorm:"column:status"` - Amount float64 `gorm:"column:amount"` - Fee float64 `gorm:"column:fee"` - Total float64 `gorm:"column:total"` - PaymentType string `gorm:"column:payment_type"` - Source string `gorm:"column:source"` - CreatedBy int64 `gorm:"column:created_by"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - OrderItems []OrderItemDB `gorm:"foreignKey:OrderID"` + ID int64 `gorm:"primaryKey;column:id"` + PartnerID int64 `gorm:"column:partner_id"` + CustomerID *int64 `gorm:"column:customer_id"` + InquiryID *string `gorm:"column:inquiry_id"` + Status string `gorm:"column:status"` + Amount float64 `gorm:"column:amount"` + Tax float64 `gorm:"column:tax"` + Total float64 `gorm:"column:total"` + PaymentType string `gorm:"column:payment_type"` + Source string `gorm:"column:source"` + CreatedBy int64 `gorm:"column:created_by"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + OrderItems []OrderItemDB `gorm:"foreignKey:OrderID"` + OrderType string `gorm:"column:order_type"` + TableNumber string `gorm:"column:table_number"` + PaymentProvider string `gorm:"column:payment_provider"` + CustomerName string `gorm:"column:customer_name"` } func (OrderDB) TableName() string { @@ -29,11 +33,13 @@ type OrderItemDB struct { ID int64 `gorm:"primaryKey;column:order_item_id"` OrderID int64 `gorm:"column:order_id"` ItemID int64 `gorm:"column:item_id"` + ItemName string `gorm:"column:item_name"` ItemType string `gorm:"column:item_type"` Price float64 `gorm:"column:price"` Quantity int `gorm:"column:quantity"` CreatedBy int64 `gorm:"column:created_by"` CreatedAt time.Time `gorm:"column:created_at"` + Product ProductDB `gorm:"foreignKey:ItemID;references:ID"` } func (OrderItemDB) TableName() string { @@ -49,7 +55,7 @@ type OrderInquiryDB struct { CustomerPhoneNumber string `gorm:"column:customer_phone_number"` Status string `gorm:"column:status"` Amount float64 `gorm:"column:amount"` - Fee float64 `gorm:"column:fee"` + Tax float64 `gorm:"column:tax"` Total float64 `gorm:"column:total"` PaymentType string `gorm:"column:payment_type"` Source string `gorm:"column:source"` @@ -72,6 +78,7 @@ type InquiryItemDB struct { InquiryID string `gorm:"column:inquiry_id"` ItemID int64 `gorm:"column:item_id"` ItemType string `gorm:"column:item_type"` + ItemName string `gorm:"column:item_name"` Price float64 `gorm:"column:price"` Quantity int `gorm:"column:quantity"` CreatedBy int64 `gorm:"column:created_by"` diff --git a/internal/repository/models/partner_setting.go b/internal/repository/models/partner_setting.go new file mode 100644 index 0000000..112d23c --- /dev/null +++ b/internal/repository/models/partner_setting.go @@ -0,0 +1,53 @@ +package models + +import ( + "time" +) + +type PartnerSettingsDB struct { + PartnerID int64 `gorm:"primaryKey;column:partner_id"` + TaxEnabled bool `gorm:"column:tax_enabled;default:false"` + TaxPercentage float64 `gorm:"column:tax_percentage;default:10.00"` + InvoicePrefix string `gorm:"column:invoice_prefix;default:INV"` + BusinessHours string `gorm:"column:business_hours;type:json"` // JSON string + LogoURL string `gorm:"column:logo_url"` + ThemeColor string `gorm:"column:theme_color;default:#000000"` + ReceiptFooterText string `gorm:"column:receipt_footer_text;type:text"` + ReceiptHeaderText string `gorm:"column:receipt_header_text;type:text"` + CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"` + UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP"` +} + +func (PartnerSettingsDB) TableName() string { + return "partner_settings" +} + +type PartnerPaymentMethodDB struct { + ID int64 `gorm:"primaryKey;column:id;autoIncrement"` + PartnerID int64 `gorm:"column:partner_id;index:idx_partner_payment"` + PaymentMethod string `gorm:"column:payment_method;index:idx_partner_payment"` + IsEnabled bool `gorm:"column:is_enabled;default:true"` + DisplayName string `gorm:"column:display_name"` + DisplayOrder int `gorm:"column:display_order;default:0"` + AdditionalInfo string `gorm:"column:additional_info;type:json"` // JSON string + CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"` + UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP"` +} + +func (PartnerPaymentMethodDB) TableName() string { + return "partner_payment_methods" +} + +type PartnerFeatureFlagDB struct { + ID int64 `gorm:"primaryKey;column:id;autoIncrement"` + PartnerID int64 `gorm:"column:partner_id;index:idx_partner_feature"` + FeatureKey string `gorm:"column:feature_key;index:idx_partner_feature"` + IsEnabled bool `gorm:"column:is_enabled;default:true"` + Config string `gorm:"column:config;type:json"` // JSON string + CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"` + UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP"` +} + +func (PartnerFeatureFlagDB) TableName() string { + return "partner_feature_flags" +} diff --git a/internal/repository/orde_repo.go b/internal/repository/orde_repo.go index 31693ec..e68da62 100644 --- a/internal/repository/orde_repo.go +++ b/internal/repository/orde_repo.go @@ -17,6 +17,26 @@ type OrderRepository interface { CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error + GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) + CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) + GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) + GetOrderPaymentMethodBreakdown( + ctx mycontext.Context, + partnerID int64, + req entity.SearchRequest, + ) ([]entity.PaymentMethodBreakdown, error) + GetRevenueOverview( + ctx mycontext.Context, + req entity.RevenueOverviewRequest, + ) ([]entity.RevenueOverviewItem, error) + GetSalesByCategory( + ctx mycontext.Context, + req entity.SalesByCategoryRequest, + ) ([]entity.SalesByCategoryItem, error) + GetPopularProducts( + ctx mycontext.Context, + req entity.PopularProductsRequest, + ) ([]entity.PopularProductItem, error) } type orderRepository struct { @@ -61,13 +81,13 @@ func (r *orderRepository) Create(ctx mycontext.Context, order *entity.Order) (*e item.ID = itemDB.ID } - if order.InProgressOrderID != "" { - if err := tx.Where("in_progress_order_id = ?", order.InProgressOrderID).Delete(&models.InProgressOrderItemDB{}).Error; err != nil { + 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.InProgressOrderDB{}).Error; err != nil { + 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") } @@ -109,6 +129,7 @@ func (r *orderRepository) CreateInquiry(ctx mycontext.Context, inquiry *entity.O InquiryID: inquiryDB.ID, ItemID: item.ItemID, ItemType: item.ItemType, + ItemName: item.ItemName, Price: item.Price, Quantity: item.Quantity, CreatedBy: item.CreatedBy, @@ -162,6 +183,7 @@ func (r *orderRepository) FindInquiryByID(ctx mycontext.Context, id string) (*en orderItems = append(orderItems, entity.OrderItem{ ItemID: itemDB.ItemID, ItemType: itemDB.ItemType, + ItemName: itemDB.ItemName, Price: itemDB.Price, Quantity: itemDB.Quantity, CreatedBy: itemDB.CreatedBy, @@ -196,38 +218,46 @@ func (r *orderRepository) UpdateInquiryStatus(ctx mycontext.Context, id string, func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB { return models.OrderDB{ - ID: order.ID, - PartnerID: order.PartnerID, - CustomerID: order.CustomerID, - InquiryID: order.InquiryID, - Status: order.Status, - Amount: order.Amount, - Fee: order.Fee, - Total: order.Total, - PaymentType: order.PaymentType, - Source: order.Source, - CreatedBy: order.CreatedBy, - CreatedAt: order.CreatedAt, - UpdatedAt: order.UpdatedAt, + ID: order.ID, + PartnerID: order.PartnerID, + CustomerID: order.CustomerID, + InquiryID: order.InquiryID, + Status: order.Status, + Amount: order.Amount, + Tax: order.Tax, + Total: order.Total, + PaymentType: order.PaymentType, + Source: order.Source, + CreatedBy: order.CreatedBy, + CreatedAt: order.CreatedAt, + UpdatedAt: order.UpdatedAt, + OrderType: order.OrderType, + TableNumber: order.TableNumber, + PaymentProvider: order.PaymentProvider, + CustomerName: order.CustomerName, } } func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order { return &entity.Order{ - ID: dbModel.ID, - PartnerID: dbModel.PartnerID, - CustomerID: dbModel.CustomerID, - InquiryID: dbModel.InquiryID, - Status: dbModel.Status, - Amount: dbModel.Amount, - Fee: dbModel.Fee, - Total: dbModel.Total, - PaymentType: dbModel.PaymentType, - Source: dbModel.Source, - CreatedBy: dbModel.CreatedBy, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - OrderItems: []entity.OrderItem{}, + ID: dbModel.ID, + PartnerID: dbModel.PartnerID, + CustomerID: dbModel.CustomerID, + InquiryID: dbModel.InquiryID, + Status: dbModel.Status, + Amount: dbModel.Amount, + Tax: dbModel.Tax, + Total: dbModel.Total, + PaymentType: dbModel.PaymentType, + Source: dbModel.Source, + CreatedBy: dbModel.CreatedBy, + CreatedAt: dbModel.CreatedAt, + UpdatedAt: dbModel.UpdatedAt, + OrderItems: []entity.OrderItem{}, + CustomerName: dbModel.CustomerName, + TableNumber: dbModel.TableNumber, + OrderType: dbModel.OrderType, + PaymentProvider: dbModel.PaymentProvider, } } @@ -237,6 +267,7 @@ func (r *orderRepository) toOrderItemDBModel(item *entity.OrderItem) models.Orde OrderID: item.OrderID, ItemID: item.ItemID, ItemType: item.ItemType, + ItemName: item.ItemName, Price: item.Price, Quantity: item.Quantity, CreatedBy: item.CreatedBy, @@ -254,6 +285,7 @@ func (r *orderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *e Quantity: dbModel.Quantity, CreatedBy: dbModel.CreatedBy, CreatedAt: dbModel.CreatedAt, + ItemName: dbModel.ItemName, } } @@ -264,7 +296,7 @@ func (r *orderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) mo CustomerID: &inquiry.CustomerID, Status: inquiry.Status, Amount: inquiry.Amount, - Fee: inquiry.Fee, + Tax: inquiry.Tax, Total: inquiry.Total, PaymentType: inquiry.PaymentType, Source: inquiry.Source, @@ -283,18 +315,22 @@ func (r *orderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) mo func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiryDB) *entity.OrderInquiry { inquiry := &entity.OrderInquiry{ - ID: dbModel.ID, - PartnerID: dbModel.PartnerID, - Status: dbModel.Status, - Amount: dbModel.Amount, - Fee: dbModel.Fee, - Total: dbModel.Total, - PaymentType: dbModel.PaymentType, - Source: dbModel.Source, - CreatedBy: dbModel.CreatedBy, - CreatedAt: dbModel.CreatedAt, - ExpiresAt: dbModel.ExpiresAt, - OrderItems: []entity.OrderItem{}, + ID: dbModel.ID, + PartnerID: dbModel.PartnerID, + Status: dbModel.Status, + Amount: dbModel.Amount, + Tax: dbModel.Tax, + Total: dbModel.Total, + PaymentType: dbModel.PaymentType, + Source: dbModel.Source, + CreatedBy: dbModel.CreatedBy, + CreatedAt: dbModel.CreatedAt, + ExpiresAt: dbModel.ExpiresAt, + OrderItems: []entity.OrderItem{}, + OrderType: dbModel.OrderType, + CustomerName: dbModel.CustomerName, + PaymentProvider: dbModel.PaymentProvider, + TableNumber: dbModel.TableNumber, } if dbModel.CustomerID != nil { @@ -305,3 +341,486 @@ func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiry return inquiry } + +func (r *orderRepository) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) { + var ordersDB []models.OrderDB + var totalCount int64 + + // Build the base query + baseQuery := r.db.Model(&models.OrderDB{}).Where("partner_id = ?", partnerID) + + // Apply filters to the base query + if req.Status != "" { + baseQuery = baseQuery.Where("status = ?", req.Status) + } + + if !req.Start.IsZero() { + baseQuery = baseQuery.Where("created_at >= ?", req.Start) + } + + if !req.End.IsZero() { + baseQuery = baseQuery.Where("created_at <= ?", req.End) + } + + // Get total count with the current filters before pagination + if err := baseQuery.Count(&totalCount).Error; err != nil { + return nil, 0, errors.Wrap(err, "failed to count total orders") + } + + // Clone the query for fetching the actual data with pagination + query := baseQuery.Session(&gorm.Session{}) + + // Add ordering and pagination + query = query.Order("created_at DESC") + + if req.Limit > 0 { + query = query.Limit(req.Limit) + } + + if req.Offset > 0 { + query = query.Offset(req.Offset) + } + + // Execute the query with preloading + if err := query.Preload("OrderItems").Find(&ordersDB).Error; err != nil { + return nil, 0, errors.Wrap(err, "failed to find order history by partner ID") + } + + // Map to domain models + orders := make([]*entity.Order, 0, len(ordersDB)) + for _, orderDB := range ordersDB { + order := r.toDomainOrderModel(&orderDB) + order.OrderItems = make([]entity.OrderItem, 0, len(orderDB.OrderItems)) + + for _, itemDB := range orderDB.OrderItems { + item := r.toDomainOrderItemModel(&itemDB) + order.OrderItems = append(order.OrderItems, *item) + } + + orders = append(orders, order) + } + + return orders, totalCount, nil +} + +func (r *orderRepository) CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) { + isUpdate := order.ID != 0 + + tx := r.db.Begin() + if tx.Error != nil { + return nil, errors.Wrap(tx.Error, "failed to begin transaction") + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + orderDB := r.toInProgressOrderDBModel(order) + + if isUpdate { + var existingOrder models.OrderDB + if err := tx.First(&existingOrder, order.ID).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "order not found for update") + } + + if err := tx.Model(&orderDB).Updates(orderDB).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to update order") + } + + 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 + } + + var itemIDs []int64 + for i := range order.OrderItems { + itemIDs = append(itemIDs, order.OrderItems[i].ItemID) + } + + var products []models.ProductDB + if len(itemIDs) > 0 { + if err := tx.Where("id IN ?", itemIDs).Find(&products).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to fetch products") + } + } + + productMap := make(map[int64]models.ProductDB) + for _, product := range products { + productMap[product.ID] = product + } + + for i := range order.OrderItems { + item := &order.OrderItems[i] + item.OrderID = orderDB.ID + itemDB := r.toOrderItemDBModel(item) + + if err := tx.Create(&itemDB).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to insert order item") + } + + item.ID = itemDB.ID + + if product, exists := productMap[item.ItemID]; exists { + item.Product = r.toDomainProductModel(&product) + } + } + + if err := tx.Commit().Error; err != nil { + return nil, errors.Wrap(err, "failed to commit transaction") + } + + return order, nil +} + +func (r *orderRepository) toInProgressOrderDBModel(order *entity.Order) models.OrderDB { + now := time.Now() + + return models.OrderDB{ + ID: order.ID, + PartnerID: order.PartnerID, + CustomerID: order.CustomerID, + CustomerName: order.CustomerName, + PaymentType: order.PaymentType, + PaymentProvider: order.PaymentProvider, + CreatedBy: order.CreatedBy, + CreatedAt: now, + UpdatedAt: now, + TableNumber: order.TableNumber, + OrderType: order.OrderType, + Status: order.Status, + Amount: order.Amount, + Total: order.Total, + Tax: order.Tax, + Source: order.Source, + } +} + +func (r *orderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product { + if productDB == nil { + return nil + } + + return &entity.Product{ + ID: productDB.ID, + Name: productDB.Name, + Description: productDB.Description, + Price: productDB.Price, + CreatedAt: productDB.CreatedAt, + UpdatedAt: productDB.UpdatedAt, + Type: productDB.Type, + Image: productDB.Image, + } +} + +func (r *orderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) { + var ordersDB []models.OrderDB + query := r.db.Where("partner_id = ?", partnerID) + + if status != "" { + query = query.Where("status = ?", status) + } + + query = query.Order("created_at DESC") + + if limit > 0 { + query = query.Limit(limit) + } + + if offset > 0 { + query = query.Offset(offset) + } + + if err := query.Find(&ordersDB).Error; err != nil { + return nil, errors.Wrap(err, "failed to find orders by partner ID") + } + + orders := make([]*entity.Order, 0, len(ordersDB)) + for _, orderDB := range ordersDB { + order := r.toDomainOrderModel(&orderDB) + + var orderItems []models.OrderItemDB + if err := r.db.Where("order_id = ?", orderDB.ID).Find(&orderItems).Error; err != nil { + return nil, errors.Wrap(err, "failed to find order items") + } + + order.OrderItems = make([]entity.OrderItem, 0, len(orderItems)) + + for _, itemDB := range orderItems { + item := r.toDomainOrderItemModel(&itemDB) + + orderItem := entity.OrderItem{ + ID: item.ID, + ItemID: item.ItemID, + Quantity: item.Quantity, + ItemName: item.ItemName, + } + + if itemDB.ItemID > 0 { + var product models.ProductDB + err := r.db.First(&product, itemDB.ItemID).Error + + if err == nil { + productDomain := r.toDomainProductModel(&product) + orderItem.Product = productDomain + } + } + + order.OrderItems = append(order.OrderItems, orderItem) + } + + orders = append(orders, order) + } + + return orders, nil +} + +func (r *orderRepository) GetOrderPaymentMethodBreakdown( + ctx mycontext.Context, + partnerID int64, + req entity.SearchRequest, +) ([]entity.PaymentMethodBreakdown, error) { + var breakdown []entity.PaymentMethodBreakdown + + baseQuery := r.db.Model(&models.OrderDB{}).Where("partner_id = ?", partnerID) + + if !req.Start.IsZero() { + baseQuery = baseQuery.Where("created_at >= ?", req.Start) + } + + if !req.End.IsZero() { + baseQuery = baseQuery.Where("created_at <= ?", req.End) + } + + if req.Status != "" { + baseQuery = baseQuery.Where("status = ?", req.Status) + } + + err := baseQuery.Select( + "payment_type, " + + "payment_provider, " + + "COUNT(*) as total_transactions, " + + "SUM(total) as total_amount", + ).Group( + "payment_type, payment_provider", + ).Order("total_amount DESC").Scan(&breakdown).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to get payment method breakdown") + } + + return breakdown, nil +} + +func (r *orderRepository) GetRevenueOverview( + ctx mycontext.Context, + req entity.RevenueOverviewRequest, +) ([]entity.RevenueOverviewItem, error) { + var overview []entity.RevenueOverviewItem + + baseQuery := r.db.Model(&models.OrderDB{}). + Where("partner_id = ?", req.PartnerID). + Where("EXTRACT(YEAR FROM created_at) = ?", req.Year) + + if req.Status != "" { + baseQuery = baseQuery.Where("status = ?", req.Status) + } + + switch req.Granularity { + case "m": // Monthly + err := baseQuery.Select( + "TO_CHAR(created_at, 'YYYY-MM') as period, " + + "SUM(total) as total_amount, " + + "COUNT(*) as order_count", + ).Group("period"). + Order("period"). + Scan(&overview).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to get monthly revenue overview") + } + + case "w": // Weekly + err := baseQuery.Select( + "CONCAT(EXTRACT(YEAR FROM created_at), '-W', " + + "LPAD(EXTRACT(WEEK FROM created_at)::text, 2, '0')) as period, " + + "SUM(total) as total_amount, " + + "COUNT(*) as order_count", + ).Group("period"). + Order("period"). + Scan(&overview).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to get weekly revenue overview") + } + + case "d": // Daily + err := baseQuery.Select( + "TO_CHAR(created_at, 'YYYY-MM-DD') as period, " + + "SUM(total) as total_amount, " + + "COUNT(*) as order_count", + ).Group("period"). + Order("period"). + Scan(&overview).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to get daily revenue overview") + } + + default: + return nil, errors.New("invalid granularity. Use 'm' (monthly), 'w' (weekly), or 'd' (daily)") + } + + return overview, nil +} + +func (r *orderRepository) GetSalesByCategory( + ctx mycontext.Context, + req entity.SalesByCategoryRequest, +) ([]entity.SalesByCategoryItem, error) { + var salesByCategory []entity.SalesByCategoryItem + + baseQuery := r.db.Model(&models.OrderItemDB{}). + Joins("JOIN orders ON order_items.order_id = orders.id"). + Where("orders.partner_id = ?", req.PartnerID) + + if req.Status != "" { + baseQuery = baseQuery.Where("orders.status = ?", req.Status) + } + + switch req.Period { + case "d": // Daily + baseQuery = baseQuery.Where("DATE(orders.created_at) = CURRENT_DATE") + case "w": // Weekly + baseQuery = baseQuery.Where("DATE_TRUNC('week', orders.created_at) = DATE_TRUNC('week', CURRENT_DATE)") + case "m": // Monthly + baseQuery = baseQuery.Where("DATE_TRUNC('month', orders.created_at) = DATE_TRUNC('month', CURRENT_DATE)") + default: + return nil, errors.New("invalid period. Use 'd' (daily), 'w' (weekly), or 'm' (monthly)") + } + + var totalSales float64 + err := r.db.Model(&models.OrderItemDB{}). + Joins("JOIN orders ON order_items.order_id = orders.id"). + Where("orders.partner_id = ?", req.PartnerID). + Select("COALESCE(SUM(order_items.price * order_items.quantity), 0)"). + Scan(&totalSales).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to calculate total sales") + } + + err = baseQuery.Select( + "order_items.item_type AS category, " + + "COALESCE(SUM(order_items.price * order_items.quantity), 0) AS total_amount, " + + "COALESCE(SUM(order_items.quantity), 0) AS total_quantity", + ). + Group("order_items.item_type"). + Order("total_amount DESC"). + Scan(&salesByCategory).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to get sales by category") + } + + for i := range salesByCategory { + if totalSales > 0 { + salesByCategory[i].Percentage = + (salesByCategory[i].TotalAmount / totalSales) * 100 + } + } + + return salesByCategory, nil +} + +func (r *orderRepository) GetPopularProducts( + ctx mycontext.Context, + req entity.PopularProductsRequest, +) ([]entity.PopularProductItem, error) { + if req.Limit == 0 { + req.Limit = 10 + } + + if req.SortBy != "sales" && req.SortBy != "revenue" { + req.SortBy = "sales" // default to sales + } + + // Base query + baseQuery := r.db.Model(&models.OrderItemDB{}). + Joins("JOIN orders ON order_items.order_id = orders.id"). + Where("orders.partner_id = ?", req.PartnerID) + + if req.Status != "" { + baseQuery = baseQuery.Where("orders.status = ?", req.Status) + } + + switch req.Period { + case "d": // Daily + baseQuery = baseQuery.Where("DATE(orders.created_at) = CURRENT_DATE") + case "w": // Weekly + baseQuery = baseQuery.Where("DATE_TRUNC('week', orders.created_at) = DATE_TRUNC('week', CURRENT_DATE)") + case "m": // Monthly + baseQuery = baseQuery.Where("DATE_TRUNC('month', orders.created_at) = DATE_TRUNC('month', CURRENT_DATE)") + default: + return nil, errors.New("invalid period. Use 'd' (daily), 'w' (weekly), or 'm' (monthly)") + } + + // Calculate total sales/revenue for percentage calculation + var totalSales struct { + TotalAmount float64 + TotalQuantity int64 + } + + err := baseQuery. + Select("COALESCE(SUM(order_items.price * order_items.quantity), 0) as total_amount, " + + "COALESCE(SUM(order_items.quantity), 0) as total_quantity"). + Scan(&totalSales).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to calculate total sales") + } + + // Prepare the query for popular products + var popularProducts []entity.PopularProductItem + orderClause := "total_sales DESC" + if req.SortBy == "revenue" { + orderClause = "total_revenue DESC" + } + + err = baseQuery. + Select( + "order_items.item_id AS product_id, " + + "order_items.item_name AS product_name, " + + "order_items.item_type AS category, " + + "COALESCE(SUM(order_items.quantity), 0) AS total_sales, " + + "COALESCE(SUM(order_items.price * order_items.quantity), 0) AS total_revenue, " + + "COALESCE(AVG(order_items.price), 0) AS average_price", + ). + Group("order_items.item_id, order_items.item_name, order_items.item_type"). + Order(orderClause). + Limit(req.Limit). + Scan(&popularProducts).Error + + if err != nil { + return nil, errors.Wrap(err, "failed to get popular products") + } + + for i := range popularProducts { + popularProducts[i].Percentage = + (float64(popularProducts[i].TotalSales) / float64(totalSales.TotalQuantity)) * 100 + } + + return popularProducts, nil +} diff --git a/internal/repository/partner_settings.go b/internal/repository/partner_settings.go new file mode 100644 index 0000000..660d67a --- /dev/null +++ b/internal/repository/partner_settings.go @@ -0,0 +1,225 @@ +package repository + +import ( + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/repository/models" + "github.com/pkg/errors" + "gorm.io/gorm" + "time" +) + +type PartnerSettingsRepository interface { + GetByPartnerID(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) + Upsert(ctx mycontext.Context, settings *entity.PartnerSettings) error + GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) + UpsertPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error + DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error + UpdatePaymentMethodOrder(ctx mycontext.Context, partnerID int64, methodIDs []int64) error +} + +type partnerSettingsRepository struct { + db *gorm.DB +} + +func NewPartnerSettingsRepository(db *gorm.DB) PartnerSettingsRepository { + return &partnerSettingsRepository{db: db} +} + +func (r *partnerSettingsRepository) GetByPartnerID(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) { + var settingsDB models.PartnerSettingsDB + + err := r.db.Where("partner_id = ?", partnerID).First(&settingsDB).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return &entity.PartnerSettings{ + PartnerID: partnerID, + TaxEnabled: true, + TaxPercentage: 10.0, + }, nil + } + return nil, errors.Wrap(err, "failed to get partner settings") + } + + return r.toDomainModel(&settingsDB), nil +} + +func (r *partnerSettingsRepository) Upsert(ctx mycontext.Context, settings *entity.PartnerSettings) error { + settingsDB := r.toDBModel(settings) + settingsDB.UpdatedAt = time.Now() + + // Check if record exists + var count int64 + if err := r.db.Model(&models.PartnerSettingsDB{}).Where("partner_id = ?", settings.PartnerID).Count(&count).Error; err != nil { + return errors.Wrap(err, "failed to check partner settings existence") + } + + if count > 0 { + // Update existing record + if err := r.db.Model(&models.PartnerSettingsDB{}).Where("partner_id = ?", settings.PartnerID).Updates(settingsDB).Error; err != nil { + return errors.Wrap(err, "failed to update partner settings") + } + } else { + // Create new record + settingsDB.CreatedAt = time.Now() + if err := r.db.Create(&settingsDB).Error; err != nil { + return errors.Wrap(err, "failed to create partner settings") + } + } + + return nil +} + +func (r *partnerSettingsRepository) GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) { + var methodsDB []models.PartnerPaymentMethodDB + + if err := r.db.Where("partner_id = ?", partnerID).Order("display_order").Find(&methodsDB).Error; err != nil { + return nil, errors.Wrap(err, "failed to get partner payment methods") + } + + methods := make([]entity.PartnerPaymentMethod, len(methodsDB)) + for i, methodDB := range methodsDB { + methods[i] = *r.toDomainPaymentMethodModel(&methodDB) + } + + return methods, nil +} + +func (r *partnerSettingsRepository) UpsertPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error { + methodDB := r.toDBPaymentMethodModel(method) + methodDB.UpdatedAt = time.Now() + + tx := r.db.Begin() + if tx.Error != nil { + return errors.Wrap(tx.Error, "failed to begin transaction") + } + + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + if method.ID > 0 { + // Update existing record + if err := tx.Model(&models.PartnerPaymentMethodDB{}).Where("id = ? AND partner_id = ?", method.ID, method.PartnerID).Updates(methodDB).Error; err != nil { + tx.Rollback() + return errors.Wrap(err, "failed to update payment method") + } + } else { + // Get the next display order if not specified + if method.DisplayOrder == 0 { + var maxOrder int + if err := tx.Model(&models.PartnerPaymentMethodDB{}).Where("partner_id = ?", method.PartnerID).Select("COALESCE(MAX(display_order), 0)").Row().Scan(&maxOrder); err != nil { + tx.Rollback() + return errors.Wrap(err, "failed to get max display order") + } + methodDB.DisplayOrder = maxOrder + 1 + } + + // Create new record + methodDB.CreatedAt = time.Now() + if err := tx.Create(&methodDB).Error; err != nil { + tx.Rollback() + return errors.Wrap(err, "failed to create payment method") + } + + method.ID = methodDB.ID + } + + return tx.Commit().Error +} + +func (r *partnerSettingsRepository) DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error { + result := r.db.Where("id = ? AND partner_id = ?", id, partnerID).Delete(&models.PartnerPaymentMethodDB{}) + + if result.Error != nil { + return errors.Wrap(result.Error, "failed to delete payment method") + } + + if result.RowsAffected == 0 { + return errors.New("payment method not found or not authorized") + } + + return nil +} + +func (r *partnerSettingsRepository) UpdatePaymentMethodOrder(ctx mycontext.Context, partnerID int64, methodIDs []int64) error { + tx := r.db.Begin() + if tx.Error != nil { + return errors.Wrap(tx.Error, "failed to begin transaction") + } + + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + for i, id := range methodIDs { + if err := tx.Model(&models.PartnerPaymentMethodDB{}). + Where("id = ? AND partner_id = ?", id, partnerID). + Update("display_order", i+1).Error; err != nil { + tx.Rollback() + return errors.Wrap(err, "failed to update payment method order") + } + } + + return tx.Commit().Error +} + +func (r *partnerSettingsRepository) toDomainModel(dbModel *models.PartnerSettingsDB) *entity.PartnerSettings { + return &entity.PartnerSettings{ + PartnerID: dbModel.PartnerID, + TaxEnabled: dbModel.TaxEnabled, + TaxPercentage: dbModel.TaxPercentage, + InvoicePrefix: dbModel.InvoicePrefix, + BusinessHours: dbModel.BusinessHours, + LogoURL: dbModel.LogoURL, + ThemeColor: dbModel.ThemeColor, + ReceiptFooterText: dbModel.ReceiptFooterText, + ReceiptHeaderText: dbModel.ReceiptHeaderText, + CreatedAt: dbModel.CreatedAt, + UpdatedAt: dbModel.UpdatedAt, + } +} + +func (r *partnerSettingsRepository) toDBModel(domainModel *entity.PartnerSettings) models.PartnerSettingsDB { + return models.PartnerSettingsDB{ + PartnerID: domainModel.PartnerID, + TaxEnabled: domainModel.TaxEnabled, + TaxPercentage: domainModel.TaxPercentage, + InvoicePrefix: domainModel.InvoicePrefix, + BusinessHours: domainModel.BusinessHours, + LogoURL: domainModel.LogoURL, + ThemeColor: domainModel.ThemeColor, + ReceiptFooterText: domainModel.ReceiptFooterText, + ReceiptHeaderText: domainModel.ReceiptHeaderText, + } +} + +func (r *partnerSettingsRepository) toDomainPaymentMethodModel(dbModel *models.PartnerPaymentMethodDB) *entity.PartnerPaymentMethod { + return &entity.PartnerPaymentMethod{ + ID: dbModel.ID, + PartnerID: dbModel.PartnerID, + PaymentMethod: dbModel.PaymentMethod, + IsEnabled: dbModel.IsEnabled, + DisplayName: dbModel.DisplayName, + DisplayOrder: dbModel.DisplayOrder, + AdditionalInfo: dbModel.AdditionalInfo, + CreatedAt: dbModel.CreatedAt, + UpdatedAt: dbModel.UpdatedAt, + } +} + +func (r *partnerSettingsRepository) toDBPaymentMethodModel(domainModel *entity.PartnerPaymentMethod) models.PartnerPaymentMethodDB { + return models.PartnerPaymentMethodDB{ + ID: domainModel.ID, + PartnerID: domainModel.PartnerID, + PaymentMethod: domainModel.PaymentMethod, + IsEnabled: domainModel.IsEnabled, + DisplayName: domainModel.DisplayName, + DisplayOrder: domainModel.DisplayOrder, + AdditionalInfo: domainModel.AdditionalInfo, + } +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index e63e723..cb0f2b6 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -58,6 +58,7 @@ type RepoManagerImpl struct { ProductRepo ProductRepository TransactionRepo TransactionRepo MemberRepository MemberRepository + PartnerSetting PartnerSettingsRepository } func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { @@ -88,6 +89,7 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { TransactionRepo: NewTransactionRepository(db), MemberRepository: NewMemberRepository(db), InProgressOrderRepo: NewInProgressOrderRepository(db), + PartnerSetting: NewPartnerSettingsRepository(db), } } diff --git a/internal/routes/customer_routes.go b/internal/routes/customer_routes.go index a366600..ae86dd1 100644 --- a/internal/routes/customer_routes.go +++ b/internal/routes/customer_routes.go @@ -2,7 +2,6 @@ package routes import ( "enaklo-pos-be/internal/handlers/http/customerauth" - "enaklo-pos-be/internal/handlers/http/customerorder" "enaklo-pos-be/internal/handlers/http/discovery" "enaklo-pos-be/internal/middlewares" @@ -20,7 +19,6 @@ func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceMan serverRoutes := []HTTPHandlerRoutes{ discovery.NewHandler(serviceManager.DiscoverService), customerauth.NewAuthHandler(serviceManager.AuthSvc, serviceManager.UserSvc, serviceManager.CustomerV2Svc), - customerorder.NewHandler(serviceManager.OrderSvc), } for _, handler := range serverRoutes { diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 626e89f..caff080 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -4,9 +4,6 @@ import ( http2 "enaklo-pos-be/internal/handlers/http" "enaklo-pos-be/internal/handlers/http/balance" "enaklo-pos-be/internal/handlers/http/license" - linkqu "enaklo-pos-be/internal/handlers/http/linqu" - mdtrns "enaklo-pos-be/internal/handlers/http/midtrans" - "enaklo-pos-be/internal/handlers/http/order" "enaklo-pos-be/internal/handlers/http/oss" "enaklo-pos-be/internal/handlers/http/partner" "enaklo-pos-be/internal/handlers/http/product" @@ -54,15 +51,12 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana user.NewHandler(serviceManager.UserSvc), studio.NewStudioHandler(serviceManager.StudioSvc), product.NewHandler(serviceManager.ProductSvc), - order.NewHandler(serviceManager.OrderSvc), oss.NewOssHandler(serviceManager.OSSSvc), partner.NewHandler(serviceManager.PartnerSvc), site.NewHandler(serviceManager.SiteSvc), - mdtrns.NewHandler(serviceManager.OrderSvc), license.NewHandler(serviceManager.LicenseSvc), transaction.New(serviceManager.Transaction), balance.NewHandler(serviceManager.Balance), - linkqu.NewHandler(serviceManager.OrderSvc), } for _, handler := range serverRoutes { diff --git a/internal/services/order/order.go b/internal/services/order/order.go deleted file mode 100644 index e3f9266..0000000 --- a/internal/services/order/order.go +++ /dev/null @@ -1,623 +0,0 @@ -package order - -import ( - "database/sql" - errors2 "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - order2 "enaklo-pos-be/internal/constants/order" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - "enaklo-pos-be/internal/utils/generator" - "encoding/json" - "errors" - "fmt" - "go.uber.org/zap" - "golang.org/x/net/context" - "gorm.io/gorm" - "strconv" - "time" -) - -type Config interface { - GetOrderFee(source string) float64 -} - -type OrderService struct { - repo repository.Order - crypt repository.Crypto - product repository.Product - pg repository.PaymentGateway - payment repository.Payment - transaction repository.TransactionRepository - txmanager repository.TransactionManager - wallet repository.WalletRepository - linkquRepo repository.LinkQu - cfg Config -} - -func NewOrderService( - repo repository.Order, - product repository.Product, crypt repository.Crypto, - pg repository.PaymentGateway, payment repository.Payment, - txmanager repository.TransactionManager, - wallet repository.WalletRepository, cfg Config, - transaction repository.TransactionRepository, - linkquRepo repository.LinkQu, -) *OrderService { - return &OrderService{ - repo: repo, - product: product, - crypt: crypt, - pg: pg, - payment: payment, - txmanager: txmanager, - wallet: wallet, - cfg: cfg, - transaction: transaction, - linkquRepo: linkquRepo, - } -} - -func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderRequest) (*entity.OrderResponse, error) { - productIDs, filteredItems := s.filterOrderItems(req.OrderItems) - if len(productIDs) == 0 { - return nil, errors2.ErrorBadRequest - } - req.OrderItems = filteredItems - - if len(productIDs) < 1 { - return nil, errors2.ErrorBadRequest - } - - products, err := s.product.GetProductsByIDs(ctx, productIDs, req.PartnerID) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err)) - return nil, err - } - - productMap := make(map[int64]*entity.ProductDB) - for _, product := range products { - productMap[product.ID] = product - } - - totalAmount := 0.0 - for _, item := range req.OrderItems { - product, ok := productMap[item.ProductID] - if !ok { - logger.ContextLogger(ctx).Error("product not found", zap.Int64("productID", item.ProductID)) - return nil, errors.New("product not found") - } - totalAmount += product.Price * float64(item.Quantity) - } - - order := &entity.Order{ - PartnerID: req.PartnerID, - Status: order2.New.String(), - Amount: totalAmount, - Total: totalAmount + s.cfg.GetOrderFee(req.Source), - Fee: s.cfg.GetOrderFee(req.Source), - PaymentType: req.PaymentMethod, - CreatedBy: req.CreatedBy, - OrderItems: []entity.OrderItem{}, - Source: req.Source, - } - - for _, item := range req.OrderItems { - order.OrderItems = append(order.OrderItems, entity.OrderItem{ - ItemID: item.ProductID, - ItemType: productMap[item.ProductID].Type, - Price: productMap[item.ProductID].Price, - Quantity: int(item.Quantity), - CreatedBy: req.CreatedBy, - Product: productMap[item.ProductID].ToProduct(), - }) - } - - order, err = s.repo.Create(ctx, order) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err)) - return nil, err - } - - order, err = s.repo.FindByID(ctx, order.ID) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err)) - return nil, err - } - - return &entity.OrderResponse{ - Order: order, - }, nil -} - -func (s *OrderService) filterOrderItems(items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest) { - var productIDs []int64 - var filteredItems []entity.OrderItemRequest - for _, item := range items { - if item.Quantity != 0 { - productIDs = append(productIDs, item.ProductID) - filteredItems = append(filteredItems, item) - } - } - return productIDs, filteredItems -} - -func (s *OrderService) CheckInInquiry(ctx mycontext.Context, qrCode string, partnerID *int64) (*entity.CheckinResponse, error) { - order, err := s.repo.FindByQRCode(ctx, qrCode) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors2.NewErrorMessage(errors2.ErrorInvalidRequest, "Not Valid QR Code") - } - - logger.ContextLogger(ctx).Error("error when getting order by QR code", zap.Error(err)) - return nil, err - } - - if order.PartnerID != *partnerID { - return nil, errors2.ErrorBadRequest - } - - if order.Status != "PAID" { - return nil, errors2.ErrorInvalidRequest - } - - token, err := s.crypt.GenerateJWTOrder(order) - if err != nil { - logger.ContextLogger(ctx).Error("error when generate checkin token", zap.Error(err)) - return nil, err - } - - orderResponse := &entity.CheckinResponse{ - Token: token, - } - - return orderResponse, nil -} - -func (s *OrderService) CheckInExecute(ctx mycontext.Context, - token string, partnerID *int64) (*entity.CheckinExecute, error) { - pID, orderID, err := s.crypt.ValidateJWTOrder(token) - if err != nil { - logger.ContextLogger(ctx).Error("error when validating JWT order", zap.Error(err)) - return nil, err - } - - if pID != *partnerID { - return nil, errors2.ErrorBadRequest - } - - order, err := s.repo.FindByID(ctx, orderID) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting order by ID", zap.Error(err)) - return nil, err - } - - resp := &entity.CheckinExecute{ - Order: order, - } - - return resp, nil -} - -func (s *OrderService) Execute(ctx mycontext.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error) { - partnerID, orderID, err := s.crypt.ValidateJWTOrder(req.Token) - if err != nil { - logger.ContextLogger(ctx).Error("error when validating JWT order", zap.Error(err)) - return nil, err - } - - order, err := s.repo.FindByID(ctx, orderID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - logger.ContextLogger(ctx).Error("order not found", zap.Int64("orderID", orderID)) - return nil, errors.New("order not found") - } - logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err)) - return nil, err - } - - payment, err := s.payment.FindByOrderAndPartnerID(ctx, orderID, partnerID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - logger.ContextLogger(ctx).Error("error getting payment data from db", zap.Error(err)) - return nil, err - } - - if payment != nil { - return s.createExecuteOrderResponse(order, payment), nil - } - - if order.PartnerID != partnerID { - logger.ContextLogger(ctx).Error("partner ID mismatch", zap.Int64("orderID", orderID), zap.Int64("tokenPartnerID", partnerID), zap.Int64("orderPartnerID", order.PartnerID)) - return nil, errors.New("partner ID mismatch") - } - - if order.Status != "NEW" { - return nil, errors.New("invalid state") - } - - resp := &entity.ExecuteOrderResponse{ - Order: order, - } - - order.SetExecutePaymentStatus() - order, err = s.repo.Update(ctx, order) - if err != nil { - logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err)) - return nil, err - } - - return resp, nil -} - -func (s *OrderService) createExecuteOrderResponse(order *entity.Order, payment *entity.Payment) *entity.ExecuteOrderResponse { - var metadata map[string]string - if err := json.Unmarshal(payment.RequestMetadata, &metadata); err != nil { - logger.ContextLogger(context.Background()).Error("error unmarshaling request metadata", zap.Error(err)) - return &entity.ExecuteOrderResponse{ - Order: order, - } - } - return &entity.ExecuteOrderResponse{ - Order: order, - PaymentToken: metadata["payment_token"], - RedirectURL: metadata["payment_redirect_url"], - } -} - -func (s *OrderService) processNonCashPayment(ctx context.Context, order *entity.Order, partnerID, createdBy int64) (*entity.MidtransResponse, error) { - paymentRequest := entity.PaymentRequest{ - PaymentReferenceID: generator.GenerateUUIDV4(), - TotalAmount: int64(order.Total), - //OrderItems: order.OrderItems, - Provider: order.PaymentType, - } - - paymentResponse, err := s.pg.CreatePayment(paymentRequest) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating payment", zap.Error(err)) - return nil, err - } - - requestMetadata, err := json.Marshal(map[string]string{ - "partner_id": strconv.FormatInt(partnerID, 10), - "created_by": strconv.FormatInt(createdBy, 10), - "payment_token": paymentResponse.Token, - "payment_redirect_url": paymentResponse.RedirectURL, - }) - if err != nil { - logger.ContextLogger(ctx).Error("error when marshaling request metadata", zap.Error(err)) - return nil, err - } - - payment := &entity.Payment{ - PartnerID: partnerID, - OrderID: order.ID, - ReferenceID: paymentRequest.PaymentReferenceID, - Channel: "MIDTRANS", - PaymentType: order.PaymentType, - Amount: order.Amount, - State: "PENDING", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - RequestMetadata: requestMetadata, - } - - _, err = s.payment.Create(ctx, payment) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating payment record", zap.Error(err)) - return nil, err - } - - return &entity.MidtransResponse{ - Token: paymentResponse.Token, - RedirectURL: paymentResponse.RedirectURL, - }, nil -} - -func (s *OrderService) processQRPayment(ctx mycontext.Context, order *entity.Order, partnerID, createdBy int64) (*entity.PaymentResponse, error) { - paymentRequest := entity.PaymentRequest{ - PaymentReferenceID: generator.GenerateUUIDV4(), - TotalAmount: int64(order.Total), - Provider: "LINKQU", - CustomerID: fmt.Sprintf("POS-%d", ctx.RequestedBy()), - CustomerName: fmt.Sprintf("POS-%s", ctx.GetName()), - } - - paymentResponse, err := s.pg.CreateQRISPayment(paymentRequest) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating payment", zap.Error(err)) - return nil, err - } - - requestMetadata, err := json.Marshal(map[string]string{ - "partner_id": strconv.FormatInt(partnerID, 10), - "created_by": strconv.FormatInt(createdBy, 10), - "qr_code": paymentResponse.QRCodeURL, - }) - - if err != nil { - logger.ContextLogger(ctx).Error("error when marshaling request metadata", zap.Error(err)) - return nil, err - } - - payment := &entity.Payment{ - PartnerID: partnerID, - OrderID: order.ID, - ReferenceID: paymentRequest.PaymentReferenceID, - Channel: "LINKQU", - PaymentType: order.PaymentType, - Amount: order.Amount, - State: "PENDING", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - RequestMetadata: requestMetadata, - } - - _, err = s.payment.Create(ctx, payment) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating payment record", zap.Error(err)) - return nil, err - } - - return paymentResponse, nil -} - -func (s *OrderService) processVAPayment(ctx mycontext.Context, order *entity.Order, partnerID, createdBy int64) (*entity.PaymentResponse, error) { - paymentRequest := entity.PaymentRequest{ - PaymentReferenceID: generator.GenerateUUIDV4(), - TotalAmount: int64(order.Total), - Provider: "LINKQU", - CustomerID: strconv.FormatInt(order.User.ID, 10), - CustomerName: order.User.Name, - CustomerEmail: order.User.Email, - } - - paymentResponse, err := s.pg.CreatePaymentVA(paymentRequest) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating payment", zap.Error(err)) - return nil, err - } - - requestMetadata, err := json.Marshal(map[string]string{ - "virtual_account": paymentResponse.VirtualAccountNumber, - "bank_name": paymentResponse.BankName, - "bank_code": paymentResponse.BankCode, - }) - - if err != nil { - logger.ContextLogger(ctx).Error("error when marshaling request metadata", zap.Error(err)) - return nil, err - } - - payment := &entity.Payment{ - PartnerID: partnerID, - OrderID: order.ID, - ReferenceID: paymentRequest.PaymentReferenceID, - Channel: "LINKQU", - PaymentType: order.PaymentType, - Amount: order.Amount, - State: "PENDING", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - RequestMetadata: requestMetadata, - } - - _, err = s.payment.Create(ctx, payment) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating payment record", zap.Error(err)) - return nil, err - } - - return paymentResponse, nil -} - -func (s *OrderService) ProcessCallback(ctx context.Context, req *entity.CallbackRequest) error { - tx, err := s.txmanager.Begin(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) - if err != nil { - return fmt.Errorf("failed to begin transaction: %w", err) - } - defer tx.Rollback() - - err = s.processPayment(ctx, tx, req) - if err != nil { - return fmt.Errorf("failed to process payment: %w", err) - } - - return tx.Commit().Error -} - -func (s *OrderService) processPayment(ctx context.Context, tx *gorm.DB, req *entity.CallbackRequest) error { - existingPayment, err := s.payment.FindByReferenceID(ctx, tx, req.TransactionID) - if err != nil { - return fmt.Errorf("failed to retrieve payment: %w", err) - } - - existingPayment.State = updatePaymentState(req.TransactionStatus) - _, err = s.payment.UpdateWithTx(ctx, tx, existingPayment) - if err != nil { - return fmt.Errorf("failed to update payment: %w", err) - } - - order, err := s.repo.FindByID(ctx, existingPayment.OrderID) - if err != nil { - return fmt.Errorf("failed to get order: %w", err) - } - - if err := s.updateOrderStatus(ctx, tx, existingPayment.State, existingPayment.OrderID); err != nil { - return fmt.Errorf("failed to update order status: %w", err) - } - - if existingPayment.State == "PAID" { - if err := s.updateWalletBalance(ctx, tx, existingPayment.PartnerID, existingPayment.Amount); err != nil { - return fmt.Errorf("failed to update wallet balance: %w", err) - } - - transaction := &entity.Transaction{ - PartnerID: existingPayment.PartnerID, - TransactionType: "PAYMENT_RECEIVED", - Status: "SUCCESS", - CreatedBy: 0, - Amount: existingPayment.Amount, - Fee: order.Fee, - Total: order.Total, - } - if _, err = s.transaction.Create(ctx, tx, transaction); err != nil { - return fmt.Errorf("failed to update transaction: %w", err) - } - } - return nil -} - -func updatePaymentState(status string) string { - switch status { - case "settlement", "capture", "paid", "settle": - return "PAID" - case "expire", "deny", "cancel", "failure", "EXPIRED": - return "EXPIRED" - default: - return status - } -} - -func (s *OrderService) updateOrderStatus(ctx context.Context, tx *gorm.DB, status string, orderID int64) error { - if status != "PENDING" { - return s.repo.SetOrderStatus(ctx, tx, orderID, status) - } - return nil -} - -func (s *OrderService) updateWalletBalance(ctx context.Context, tx *gorm.DB, partnerID int64, amount float64) error { - wallet, err := s.wallet.GetByPartnerID(ctx, tx, partnerID) - if err != nil { - return fmt.Errorf("failed to get wallet: %w", err) - } - wallet.Balance += amount - _, err = s.wallet.Update(ctx, tx, wallet) - return err -} - -func (s *OrderService) GetAllHistoryOrders(ctx mycontext.Context, req entity.OrderSearch) ([]*entity.HistoryOrder, int, error) { - historyOrders, total, err := s.repo.GetAllHystoryOrders(ctx, req) - if err != nil { - logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err)) - return nil, 0, err - } - - data := historyOrders.ToHistoryOrderList() - - return data, total, nil -} - -func (s *OrderService) CountSoldOfTicket(ctx mycontext.Context, req entity.OrderSearch) (*entity.TicketSold, error) { - ticket, err := s.repo.CountSoldOfTicket(ctx, req) - - if err != nil { - logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err)) - return nil, err - } - - data := ticket.ToTicketSold() - - return data, nil -} - -func (s *OrderService) GetDailySales(ctx mycontext.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error) { - dailySales, err := s.repo.GetDailySalesMetrics(ctx, req) - - if err != nil { - logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err)) - return nil, err - } - - return dailySales, nil -} - -func (s *OrderService) GetPaymentDistribution(ctx mycontext.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) { - paymentDistribution, err := s.repo.GetPaymentTypeDistribution(ctx, req) - - if err != nil { - logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err)) - return nil, err - } - - return paymentDistribution, nil -} - -func (s *OrderService) SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.Order, error) { - amount, err := s.repo.SumAmount(ctx, req) - - if err != nil { - logger.ContextLogger(ctx).Error("error when get amount cash orders", zap.Error(err)) - return nil, err - } - - data := amount.ToSumAmount() - - return data, nil -} - -func (s *OrderService) GetByID(ctx mycontext.Context, id int64, referenceID string) (*entity.Order, error) { - if referenceID != "" { - payment, err := s.payment.FindByReferenceID(ctx, nil, referenceID) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting payment by IDs", zap.Error(err)) - return nil, err - } - id = payment.OrderID - } - - 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 ctx.IsCasheer() { - return order, nil - } - - //if order.CreatedBy != ctx.RequestedBy() { - // return nil, errors2.NewError(errors2.ErrorBadRequest.ErrorType(), "order not found") - //} - - return order, nil -} - -func (s *OrderService) GetPrintDetail(ctx mycontext.Context, id int64) (*entity.OrderPrintDetail, error) { - order, err := s.repo.FindPrintDetailByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err)) - return nil, err - } - - return order, nil -} - -func (s *OrderService) ProcessLinkQuCallback(ctx context.Context, req *entity.LinkQuCallback) error { - tx, err := s.txmanager.Begin(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) - if err != nil { - return fmt.Errorf("failed to begin transaction: %w", err) - } - defer tx.Rollback() - - pay, err := s.linkquRepo.CheckPaymentStatus(req.PaymentReff) - if err != nil { - return fmt.Errorf("failed to begin transaction: %w", err) - } - - if pay.ResponseCode != "00" { - return nil - } - - err = s.processPayment(ctx, tx, &entity.CallbackRequest{ - TransactionID: req.PartnerReff, - TransactionStatus: pay.Data.StatusPaid, - }) - - if err != nil { - return fmt.Errorf("failed to process payment: %w", err) - } - - return tx.Commit().Error -} diff --git a/internal/services/service.go b/internal/services/service.go index cb7180e..0caf205 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -7,7 +7,6 @@ import ( "enaklo-pos-be/internal/services/discovery" service "enaklo-pos-be/internal/services/license" "enaklo-pos-be/internal/services/member" - "enaklo-pos-be/internal/services/order" "enaklo-pos-be/internal/services/oss" "enaklo-pos-be/internal/services/partner" "enaklo-pos-be/internal/services/product" @@ -18,6 +17,7 @@ import ( 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" "gorm.io/gorm" @@ -35,7 +35,6 @@ type ServiceManagerImpl struct { UserSvc User StudioSvc Studio ProductSvc Product - OrderSvc Order OSSSvc OSSService PartnerSvc Partner SiteSvc Site @@ -55,15 +54,16 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) custSvcV2 := customerSvc.New(repo.CustomerRepo, repo.EmailService) productSvcV2 := productSvc.New(repo.ProductRepo) - inprogressOrder := inprogress_order.NewInProgressOrderService(repo.InProgressOrderRepo) + partnerSettings := partner_settings.NewPartnerSettingsService(repo.PartnerSetting) + orderService := orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings) + inprogressOrder := inprogress_order.NewInProgressOrderService(repo.OrderRepo, orderService, productSvcV2) return &ServiceManagerImpl{ AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License), EventSvc: event.NewEventService(repo.Event), UserSvc: users.NewUserService(repo.User), StudioSvc: studio.NewStudioService(repo.Studio), ProductSvc: product.NewProductService(repo.Product), - OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Crypto, repo.PG, repo.Payment, repo.Trx, repo.Wallet, &cfg.Order, repo.Transaction, repo.LinkQu), OSSSvc: oss.NewOSSService(repo.OSS), PartnerSvc: partner.NewPartnerService( repo.Partner, users.NewUserService(repo.User), repo.Trx, repo.Wallet, repo.User), @@ -72,7 +72,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) 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, repo.Product), - OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService), + OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings), MemberRegistrationSvc: member.NewMemberRegistrationService(repo.MemberRepository, repo.EmailService, custSvcV2), CustomerV2Svc: custSvcV2, InProgressSvc: inprogressOrder, diff --git a/internal/services/v2/inprogress_order/in_progress_order.go b/internal/services/v2/inprogress_order/in_progress_order.go index 1856c62..0ea834d 100644 --- a/internal/services/v2/inprogress_order/in_progress_order.go +++ b/internal/services/v2/inprogress_order/in_progress_order.go @@ -3,28 +3,98 @@ package inprogress_order import ( "enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/mycontext" + order2 "enaklo-pos-be/internal/constants/order" "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" + "enaklo-pos-be/internal/services/v2/order" "github.com/pkg/errors" "go.uber.org/zap" ) type InProgressOrderService interface { - Save(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) - GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) + Save(ctx mycontext.Context, order *entity.OrderRequest) (*entity.Order, error) + GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*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) +} + +type OrderCalculator interface { + CalculateOrderTotals( + ctx mycontext.Context, + items []entity.OrderItemRequest, + productDetails *entity.ProductDetails, + source string, + ) (*entity.OrderCalculation, error) + ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) } type inProgressOrderSvc struct { - repo repository.InProgressOrderRepository + repo OrderRepository + orderCalculator OrderCalculator + product order.ProductService } -func NewInProgressOrderService(repo repository.InProgressOrderRepository) InProgressOrderService { +func NewInProgressOrderService(repo OrderRepository, calculator OrderCalculator, product order.ProductService) InProgressOrderService { return &inProgressOrderSvc{ - repo: repo, + repo: repo, + orderCalculator: calculator, + product: product, } } -func (s *inProgressOrderSvc) Save(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) { +func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) { + productIDs, filteredItems, err := s.orderCalculator.ValidateOrderItems(ctx, req.OrderItems) + if err != nil { + return nil, err + } + req.OrderItems = filteredItems + + productDetails, err := s.product.GetProductDetails(ctx, productIDs, req.PartnerID) + if err != nil { + logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err)) + return nil, err + } + + orderCalculation, err := s.orderCalculator.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source) + if err != nil { + return nil, err + } + + orderItems := make([]entity.OrderItem, len(req.OrderItems)) + for i, item := range req.OrderItems { + product, exists := productDetails.Products[item.ProductID] + productName := "" + if exists { + productName = product.Name + } + + orderItems[i] = entity.OrderItem{ + ItemID: item.ProductID, + ItemName: productName, + Quantity: item.Quantity, + Price: product.Price, + ItemType: product.Type, + } + } + + order := &entity.Order{ + ID: req.ID, + PartnerID: req.PartnerID, + CustomerID: req.CustomerID, + CustomerName: req.CustomerName, + CreatedBy: req.CreatedBy, + OrderItems: orderItems, + TableNumber: req.TableNumber, + OrderType: req.OrderType, + Total: orderCalculation.Total, + Tax: orderCalculation.Tax, + Amount: orderCalculation.Subtotal, + Status: order2.Pending.String(), + Source: req.Source, + } + createdOrder, err := s.repo.CreateOrUpdate(ctx, order) if err != nil { logger.ContextLogger(ctx).Error("failed to create in-progress order", @@ -36,8 +106,8 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, order *entity.InProgres return createdOrder, nil } -func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) { - orders, err := s.repo.GetListByPartnerID(ctx, partnerID, limit, offset) +func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) { + orders, err := s.repo.GetListByPartnerID(ctx, partnerID, limit, offset, order2.Pending.String()) if err != nil { logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID", zap.Error(err), diff --git a/internal/services/v2/order/create_order_inquiry.go b/internal/services/v2/order/create_order_inquiry.go index b9895e1..9e6f5e5 100644 --- a/internal/services/v2/order/create_order_inquiry.go +++ b/internal/services/v2/order/create_order_inquiry.go @@ -7,11 +7,12 @@ import ( "enaklo-pos-be/internal/constants" "enaklo-pos-be/internal/entity" "go.uber.org/zap" + "math" ) func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context, req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) { - productIDs, filteredItems, err := s.validateOrderItems(ctx, req.OrderItems) + productIDs, filteredItems, err := s.ValidateOrderItems(ctx, req.OrderItems) if err != nil { return nil, err } @@ -23,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) if err != nil { return nil, err } @@ -43,7 +44,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context, req.PartnerID, customerID, orderCalculation.Subtotal, - orderCalculation.Fee, + orderCalculation.Tax, orderCalculation.Total, req.PaymentMethod, req.Source, @@ -79,7 +80,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context, }, nil } -func (s *orderSvc) validateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) { +func (s *orderSvc) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) { var productIDs []int64 var filteredItems []entity.OrderItemRequest @@ -98,7 +99,7 @@ func (s *orderSvc) validateOrderItems(ctx mycontext.Context, items []entity.Orde return productIDs, filteredItems, nil } -func (s *orderSvc) calculateOrderTotals( +func (s *orderSvc) CalculateOrderTotals( ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, @@ -114,12 +115,23 @@ func (s *orderSvc) calculateOrderTotals( subtotal += product.Price * float64(item.Quantity) } - fee := s.cfg.GetOrderFee(source) + partnerID := ctx.GetPartnerID() + setting, err := s.partnerSetting.GetSettings(ctx, *partnerID) + + if err != nil { + return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "failed to get partner settings") + } + + tax := 0.0 + if setting.TaxEnabled { + tax = (setting.TaxPercentage / 100) * subtotal + tax = math.Round(tax/100) * 100 + } return &entity.OrderCalculation{ Subtotal: subtotal, - Fee: fee, - Total: subtotal + fee, + Tax: tax, + Total: subtotal + tax, }, nil } @@ -145,3 +157,79 @@ func (s *orderSvc) validateInquiry(ctx mycontext.Context, token string) (*entity return inquiry, nil } + +func (s *orderSvc) GetOrderPaymentAnalysis( + ctx mycontext.Context, + partnerID int64, + req entity.SearchRequest, +) (*entity.OrderPaymentAnalysis, error) { + paymentBreakdown, err := s.repo.GetOrderPaymentMethodBreakdown(ctx, partnerID, req) + if err != nil { + return nil, err + } + + var totalAmount float64 + var totalTransactions int64 + + for _, breakdown := range paymentBreakdown { + totalAmount += breakdown.TotalAmount + totalTransactions += breakdown.TotalTransactions + } + + return &entity.OrderPaymentAnalysis{ + TotalAmount: totalAmount, + TotalTransactions: totalTransactions, + PaymentMethodBreakdown: paymentBreakdown, + }, nil +} + +func (s *orderSvc) GetRevenueOverview( + ctx mycontext.Context, + partnerID int64, + year int, + granularity string, + status string, +) ([]entity.RevenueOverviewItem, error) { + req := entity.RevenueOverviewRequest{ + PartnerID: partnerID, + Year: year, + Granularity: granularity, + Status: status, + } + + return s.repo.GetRevenueOverview(ctx, req) +} + +func (s *orderSvc) GetSalesByCategory( + ctx mycontext.Context, + partnerID int64, + period string, + status string, +) ([]entity.SalesByCategoryItem, error) { + req := entity.SalesByCategoryRequest{ + PartnerID: partnerID, + Period: period, + Status: status, + } + + return s.repo.GetSalesByCategory(ctx, req) +} + +func (s *orderSvc) GetPopularProducts( + ctx mycontext.Context, + partnerID int64, + period string, + status string, + limit int, + sortBy string, +) ([]entity.PopularProductItem, error) { + req := entity.PopularProductsRequest{ + PartnerID: partnerID, + Period: period, + Status: status, + Limit: limit, + SortBy: sortBy, + } + + return s.repo.GetPopularProducts(ctx, req) +} diff --git a/internal/services/v2/order/execute_order.go b/internal/services/v2/order/execute_order.go index 2c29d9b..268ea41 100644 --- a/internal/services/v2/order/execute_order.go +++ b/internal/services/v2/order/execute_order.go @@ -10,7 +10,7 @@ import ( ) func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context, - token string, paymentMethod, paymentProvider, inprogressOrderID string) (*entity.OrderResponse, error) { + token string, paymentMethod, paymentProvider string, inprogressOrderID int64) (*entity.OrderResponse, error) { inquiry, err := s.validateInquiry(ctx, token) if err != nil { return nil, err diff --git a/internal/services/v2/order/order.go b/internal/services/v2/order/order.go index 6de092b..966a82c 100644 --- a/internal/services/v2/order/order.go +++ b/internal/services/v2/order/order.go @@ -12,6 +12,24 @@ type Repository interface { CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error + GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) + GetOrderPaymentMethodBreakdown( + ctx mycontext.Context, + partnerID int64, + req entity.SearchRequest, + ) ([]entity.PaymentMethodBreakdown, error) + GetRevenueOverview( + ctx mycontext.Context, + req entity.RevenueOverviewRequest, + ) ([]entity.RevenueOverviewItem, error) + GetSalesByCategory( + ctx mycontext.Context, + req entity.SalesByCategoryRequest, + ) ([]entity.SalesByCategoryItem, error) + GetPopularProducts( + ctx mycontext.Context, + req entity.PopularProductsRequest, + ) ([]entity.PopularProductItem, error) } type ProductService interface { @@ -42,21 +60,65 @@ type Service interface { CreateOrderInquiry(ctx mycontext.Context, req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) ExecuteOrderInquiry(ctx mycontext.Context, - token string, paymentMethod, paymentProvider, inProgressOrderID string) (*entity.OrderResponse, error) + token string, paymentMethod, paymentProvider string, inProgressOrderID int64) (*entity.OrderResponse, error) + GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) + CalculateOrderTotals( + ctx mycontext.Context, + items []entity.OrderItemRequest, + productDetails *entity.ProductDetails, + source string, + ) (*entity.OrderCalculation, error) + ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) + GetOrderPaymentAnalysis( + ctx mycontext.Context, + partnerID int64, + req entity.SearchRequest, + ) (*entity.OrderPaymentAnalysis, error) + GetRevenueOverview( + ctx mycontext.Context, + partnerID int64, + year int, + granularity string, + status string, + ) ([]entity.RevenueOverviewItem, error) + GetSalesByCategory( + ctx mycontext.Context, + partnerID int64, + period string, + status string, + ) ([]entity.SalesByCategoryItem, error) + GetPopularProducts( + ctx mycontext.Context, + partnerID int64, + period string, + status string, + limit int, + sortBy string, + ) ([]entity.PopularProductItem, error) } type Config interface { GetOrderFee(source string) float64 } +type PartnerSettings interface { + GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) +} + +type InProgressOrderRepository interface { + GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) +} + type orderSvc struct { - repo Repository - product ProductService - customer CustomerService - transaction TransactionService - crypt CryptService - cfg Config - notification NotificationService + repo Repository + product ProductService + customer CustomerService + transaction TransactionService + crypt CryptService + cfg Config + notification NotificationService + partnerSetting PartnerSettings + inprogressOrder InProgressOrderRepository } func New( @@ -67,14 +129,16 @@ func New( crypt CryptService, cfg Config, notification NotificationService, + partnerSetting PartnerSettings, ) Service { return &orderSvc{ - repo: repo, - product: product, - customer: customer, - transaction: transaction, - crypt: crypt, - cfg: cfg, - notification: notification, + repo: repo, + product: product, + customer: customer, + transaction: transaction, + crypt: crypt, + cfg: cfg, + notification: notification, + partnerSetting: partnerSetting, } } diff --git a/internal/services/v2/order/order_history.go b/internal/services/v2/order/order_history.go new file mode 100644 index 0000000..d6b1a55 --- /dev/null +++ b/internal/services/v2/order/order_history.go @@ -0,0 +1,10 @@ +package order + +import ( + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" +) + +func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) { + return s.repo.GetOrderHistoryByPartnerID(ctx, partnerID, request) +} diff --git a/internal/services/v2/partner_settings/partner_setting.go b/internal/services/v2/partner_settings/partner_setting.go new file mode 100644 index 0000000..ea88542 --- /dev/null +++ b/internal/services/v2/partner_settings/partner_setting.go @@ -0,0 +1,149 @@ +package partner_settings + +import ( + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/repository" + "encoding/json" + "github.com/pkg/errors" +) + +type PartnerSettingsService interface { + GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) + UpdateSettings(ctx mycontext.Context, settings *entity.PartnerSettings) error + GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) + AddPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error + UpdatePaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error + DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error + ReorderPaymentMethods(ctx mycontext.Context, partnerID int64, methodIDs []int64) error + GetBusinessHours(ctx mycontext.Context, partnerID int64) (*entity.BusinessHoursSetting, error) + UpdateBusinessHours(ctx mycontext.Context, partnerID int64, hours *entity.BusinessHoursSetting) error +} + +type partnerSettingsService struct { + settingsRepo repository.PartnerSettingsRepository +} + +func NewPartnerSettingsService(settingsRepo repository.PartnerSettingsRepository) PartnerSettingsService { + return &partnerSettingsService{ + settingsRepo: settingsRepo, + } +} + +func (s *partnerSettingsService) GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) { + return s.settingsRepo.GetByPartnerID(ctx, partnerID) +} + +func (s *partnerSettingsService) UpdateSettings(ctx mycontext.Context, settings *entity.PartnerSettings) error { + if settings == nil { + return errors.New("settings cannot be nil") + } + + // Validate tax percentage + if settings.TaxEnabled && (settings.TaxPercentage < 0 || settings.TaxPercentage > 100) { + return errors.New("tax percentage must be between 0 and 100") + } + + return s.settingsRepo.Upsert(ctx, settings) +} + +func (s *partnerSettingsService) GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) { + return s.settingsRepo.GetPaymentMethods(ctx, partnerID) +} + +func (s *partnerSettingsService) AddPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error { + if method == nil { + return errors.New("payment method cannot be nil") + } + + method.ID = 0 + + return s.settingsRepo.UpsertPaymentMethod(ctx, method) +} + +func (s *partnerSettingsService) UpdatePaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error { + if method == nil { + return errors.New("payment method cannot be nil") + } + + if method.ID <= 0 { + return errors.New("invalid payment method ID") + } + + return s.settingsRepo.UpsertPaymentMethod(ctx, method) +} + +func (s *partnerSettingsService) DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error { + if id <= 0 { + return errors.New("invalid payment method ID") + } + + return s.settingsRepo.DeletePaymentMethod(ctx, id, partnerID) +} + +func (s *partnerSettingsService) ReorderPaymentMethods(ctx mycontext.Context, partnerID int64, methodIDs []int64) error { + if len(methodIDs) == 0 { + return errors.New("method IDs cannot be empty") + } + + return s.settingsRepo.UpdatePaymentMethodOrder(ctx, partnerID, methodIDs) +} + +// GetBusinessHours retrieves parsed business hours for a partner +func (s *partnerSettingsService) GetBusinessHours(ctx mycontext.Context, partnerID int64) (*entity.BusinessHoursSetting, error) { + settings, err := s.settingsRepo.GetByPartnerID(ctx, partnerID) + if err != nil { + return nil, err + } + + // Create default hours if not set + if settings.BusinessHours == "" { + defaultHours := createDefaultBusinessHours() + return defaultHours, nil + } + + var hours entity.BusinessHoursSetting + if err := json.Unmarshal([]byte(settings.BusinessHours), &hours); err != nil { + return nil, errors.Wrap(err, "failed to parse business hours") + } + + return &hours, nil +} + +func (s *partnerSettingsService) UpdateBusinessHours(ctx mycontext.Context, partnerID int64, hours *entity.BusinessHoursSetting) error { + if hours == nil { + return errors.New("business hours cannot be nil") + } + + settings, err := s.settingsRepo.GetByPartnerID(ctx, partnerID) + if err != nil { + return err + } + + // Serialize hours to JSON + hoursJSON, err := json.Marshal(hours) + if err != nil { + return errors.Wrap(err, "failed to serialize business hours") + } + + settings.BusinessHours = string(hoursJSON) + + return s.settingsRepo.Upsert(ctx, settings) +} + +func createDefaultBusinessHours() *entity.BusinessHoursSetting { + defaultDay := entity.DayHours{ + Open: "08:00", + Close: "22:00", + } + + return &entity.BusinessHoursSetting{ + Monday: defaultDay, + Tuesday: defaultDay, + Wednesday: defaultDay, + Thursday: defaultDay, + Friday: defaultDay, + Saturday: defaultDay, + Sunday: defaultDay, + } +}