This commit is contained in:
aditya.siregar 2025-04-10 11:21:08 +07:00
parent c642c5c61b
commit 09c9a4d59d
30 changed files with 1797 additions and 758 deletions

View File

@ -6,6 +6,7 @@ const (
New OrderStatus = "NEW" New OrderStatus = "NEW"
Paid OrderStatus = "PAID" Paid OrderStatus = "PAID"
Cancel OrderStatus = "CANCEL" Cancel OrderStatus = "CANCEL"
Pending OrderStatus = "PENDING"
) )
func (b OrderStatus) toString() string { func (b OrderStatus) toString() string {

View File

@ -10,7 +10,7 @@ type Order struct {
Status string `gorm:"type:varchar;column:status"` Status string `gorm:"type:varchar;column:status"`
Amount float64 `gorm:"type:numeric;not null;column:amount"` Amount float64 `gorm:"type:numeric;not null;column:amount"`
Total float64 `gorm:"type:numeric;not null;column:total"` 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 CustomerID *int64
CustomerName string CustomerName string
InquiryID *string InquiryID *string
@ -27,7 +27,7 @@ type Order struct {
Source string `gorm:"type:varchar;column:source"` Source string `gorm:"type:varchar;column:source"`
OrderType string `gorm:"type:varchar;column:order_type"` OrderType string `gorm:"type:varchar;column:order_type"`
TableNumber string TableNumber string
InProgressOrderID string InProgressOrderID int64
} }
type OrderDB struct { type OrderDB struct {
@ -86,6 +86,7 @@ type OrderItem struct {
CreatedBy int64 `gorm:"type:int;column:created_by"` CreatedBy int64 `gorm:"type:int;column:created_by"`
UpdatedBy int64 `gorm:"type:int;column:updated_by"` UpdatedBy int64 `gorm:"type:int;column:updated_by"`
Product *Product `gorm:"foreignKey:ItemID;references:ID"` Product *Product `gorm:"foreignKey:ItemID;references:ID"`
ItemName string `gorm:"type:varchar;column:item_name"`
} }
func (OrderItem) TableName() string { func (OrderItem) TableName() string {
@ -105,6 +106,7 @@ type OrderRequest struct {
TableNumber string TableNumber string
PaymentProvider string PaymentProvider string
OrderType string OrderType string
ID int64
} }
type OrderItemRequest struct { type OrderItemRequest struct {

View File

@ -14,7 +14,7 @@ type OrderInquiry struct {
CustomerEmail string `json:"customer_email"` CustomerEmail string `json:"customer_email"`
Status string `json:"status"` Status string `json:"status"`
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Fee float64 `json:"fee"` Tax float64 `json:"tax"`
Total float64 `json:"total"` Total float64 `json:"total"`
PaymentType string `json:"payment_type"` PaymentType string `json:"payment_type"`
Source string `json:"source"` Source string `json:"source"`
@ -30,7 +30,7 @@ type OrderInquiry struct {
type OrderCalculation struct { type OrderCalculation struct {
Subtotal float64 `json:"subtotal"` Subtotal float64 `json:"subtotal"`
Fee float64 `json:"fee"` Tax float64 `json:"tax"`
Total float64 `json:"total"` Total float64 `json:"total"`
} }
@ -43,7 +43,7 @@ func NewOrderInquiry(
partnerID int64, partnerID int64,
customerID int64, customerID int64,
amount float64, amount float64,
fee float64, tax float64,
total float64, total float64,
paymentType string, paymentType string,
source string, source string,
@ -60,7 +60,7 @@ func NewOrderInquiry(
PartnerID: partnerID, PartnerID: partnerID,
Status: "PENDING", Status: "PENDING",
Amount: amount, Amount: amount,
Fee: fee, Tax: tax,
Total: total, Total: total,
PaymentType: paymentType, PaymentType: paymentType,
CustomerID: customerID, CustomerID: customerID,
@ -83,6 +83,7 @@ func (oi *OrderInquiry) AddOrderItem(item OrderItemRequest, product *Product) {
ItemID: item.ProductID, ItemID: item.ProductID,
ItemType: product.Type, ItemType: product.Type,
Price: product.Price, Price: product.Price,
ItemName: product.Name,
Quantity: item.Quantity, Quantity: item.Quantity,
CreatedBy: oi.CreatedBy, CreatedBy: oi.CreatedBy,
Product: product, Product: product,
@ -98,7 +99,7 @@ func (i *OrderInquiry) ToOrder(paymentMethod, paymentProvider string) *Order {
InquiryID: &i.ID, InquiryID: &i.ID,
Status: constants.StatusPaid, Status: constants.StatusPaid,
Amount: i.Amount, Amount: i.Amount,
Fee: i.Fee, Tax: i.Tax,
Total: i.Total, Total: i.Total,
PaymentType: paymentMethod, PaymentType: paymentMethod,
PaymentProvider: paymentProvider, PaymentProvider: paymentProvider,
@ -107,6 +108,8 @@ func (i *OrderInquiry) ToOrder(paymentMethod, paymentProvider string) *Order {
CreatedAt: now, CreatedAt: now,
OrderItems: make([]OrderItem, len(i.OrderItems)), OrderItems: make([]OrderItem, len(i.OrderItems)),
OrderType: i.OrderType, OrderType: i.OrderType,
CustomerName: i.CustomerName,
TableNumber: i.TableNumber,
} }
for idx, item := range i.OrderItems { for idx, item := range i.OrderItems {
@ -114,6 +117,7 @@ func (i *OrderInquiry) ToOrder(paymentMethod, paymentProvider string) *Order {
ItemID: item.ItemID, ItemID: item.ItemID,
ItemType: item.ItemType, ItemType: item.ItemType,
Price: item.Price, Price: item.Price,
ItemName: item.ItemName,
Quantity: item.Quantity, Quantity: item.Quantity,
CreatedBy: i.CreatedBy, CreatedBy: i.CreatedBy,
CreatedAt: now, CreatedAt: now,

View File

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

View File

@ -128,3 +128,39 @@ type ProductDetails struct {
Products map[int64]*Product // Map for quick lookups by ID Products map[int64]*Product // Map for quick lookups by ID
PartnerID int64 // Common site ID for all products 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"`
}

32
internal/entity/search.go Normal file
View File

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

View File

@ -109,7 +109,7 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse, req requ
PaymentType: order.PaymentType, PaymentType: order.PaymentType,
CreatedAt: order.CreatedAt, CreatedAt: order.CreatedAt,
OrderItems: orderItems, OrderItems: orderItems,
Fee: order.Fee, Tax: order.Tax,
Total: order.Total, Total: order.Total,
} }
} }
@ -249,7 +249,7 @@ func (h *Handler) toOrderDetail(order *entity.Order) *response.OrderDetail {
PaymentLink: paymentLink, PaymentLink: paymentLink,
PaymentToken: paymentToken, PaymentToken: paymentToken,
SiteName: siteName, SiteName: siteName,
Fee: order.Fee, Fee: order.Tax,
} }
orderDetail.OrderItems = make([]response.OrderDetailItem, len(order.OrderItems)) orderDetail.OrderItems = make([]response.OrderDetailItem, len(order.OrderItems))

View File

@ -38,7 +38,7 @@ type CreateInProgressOrderRequest struct {
OrderType string `json:"order_type"` OrderType string `json:"order_type"`
PaymentProvider string `json:"payment_provider"` PaymentProvider string `json:"payment_provider"`
TableNumber string `json:"table_number"` TableNumber string `json:"table_number"`
InProgressOrderID string `json:"in_progress_order_id"` InProgressOrderID int64 `json:"in_progress_order_id"`
} }
type InProgressOrderItemRequest struct { type InProgressOrderItemRequest struct {
@ -72,15 +72,15 @@ func (h *InProgressOrderHandler) Save(c *gin.Context) {
return return
} }
orderItems := make([]entity.InProgressOrderItem, len(req.OrderItems)) orderItems := make([]entity.OrderItemRequest, len(req.OrderItems))
for i, item := range req.OrderItems { for i, item := range req.OrderItems {
orderItems[i] = entity.InProgressOrderItem{ orderItems[i] = entity.OrderItemRequest{
ItemID: item.ProductID, ProductID: item.ProductID,
Quantity: item.Quantity, Quantity: item.Quantity,
} }
} }
order := &entity.InProgressOrder{ order := &entity.OrderRequest{
PartnerID: *partnerID, PartnerID: *partnerID,
CustomerID: req.CustomerID, CustomerID: req.CustomerID,
CustomerName: req.CustomerName, CustomerName: req.CustomerName,
@ -89,6 +89,7 @@ func (h *InProgressOrderHandler) Save(c *gin.Context) {
TableNumber: req.TableNumber, TableNumber: req.TableNumber,
OrderType: req.OrderType, OrderType: req.OrderType,
ID: req.InProgressOrderID, ID: req.InProgressOrderID,
Source: "POS",
} }
_, err := h.service.Save(ctx, order) _, 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)) orderItems := make([]map[string]interface{}, len(order.OrderItems))
for i, item := range order.OrderItems { for i, item := range order.OrderItems {
orderItems[i] = map[string]interface{}{ orderItems[i] = map[string]interface{}{

View File

@ -2,6 +2,7 @@ package http
import ( import (
"enaklo-pos-be/internal/common/errors" "enaklo-pos-be/internal/common/errors"
order2 "enaklo-pos-be/internal/constants/order"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/handlers/request" "enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/handlers/response" "enaklo-pos-be/internal/handlers/response"
@ -9,6 +10,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"net/http" "net/http"
"strconv"
"time"
) )
type Handler struct { 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("/inquiry", jwt, h.Inquiry)
route.POST("/execute", jwt, h.Execute) 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 { type InquiryRequest struct {
@ -56,7 +65,7 @@ type OrderItemRequest struct {
type ExecuteRequest struct { type ExecuteRequest struct {
PaymentMethod string `json:"payment_method" validate:"required"` PaymentMethod string `json:"payment_method" validate:"required"`
PaymentProvider string `json:"payment_provider"` PaymentProvider string `json:"payment_provider"`
InProgressOrderID string `json:"in_progress_order_id"` InProgressOrderID int64 `json:"in_progress_order_id"`
Token string `json:"token"` Token string `json:"token"`
} }
@ -140,3 +149,307 @@ func (h *Handler) Execute(c *gin.Context) {
Data: response.MapToOrderResponse(result), 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,
})
}

View File

@ -204,7 +204,7 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response
Status: order.Status, Status: order.Status,
Amount: order.Amount, Amount: order.Amount,
Total: order.Total, Total: order.Total,
Fee: order.Fee, Tax: order.Tax,
PaymentType: order.PaymentType, PaymentType: order.PaymentType,
CreatedAt: order.CreatedAt, CreatedAt: order.CreatedAt,
OrderItems: orderItems, OrderItems: orderItems,

View File

@ -28,6 +28,7 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route.PUT("/:id", jwt, isSuperAdmin, h.Update) route.PUT("/:id", jwt, isSuperAdmin, h.Update)
route.GET("/:id", jwt, isSuperAdmin, h.GetByID) route.GET("/:id", jwt, isSuperAdmin, h.GetByID)
route.DELETE("/:id", jwt, isSuperAdmin, h.Delete) route.DELETE("/:id", jwt, isSuperAdmin, h.Delete)
route.PUT("/update", jwt, h.UpdateMyStore)
} }
func NewHandler(service services.Partner) *Handler { func NewHandler(service services.Partner) *Handler {
@ -268,3 +269,27 @@ func (h *Handler) toPartnerResponseList(resp []*entity.Partner, total int64, req
Offset: req.Offset, 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),
})
}

View File

@ -88,7 +88,7 @@ type CreateOrderResponse struct {
Status string `json:"status"` Status string `json:"status"`
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Total float64 `json:"total"` Total float64 `json:"total"`
Fee float64 `json:"fee"` Tax float64 `json:"tax"`
PaymentType string `json:"payment_type"` PaymentType string `json:"payment_type"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
OrderItems []CreateOrderItemResponse `json:"order_items"` OrderItems []CreateOrderItemResponse `json:"order_items"`
@ -187,3 +187,17 @@ type OrderDetailItem struct {
UnitPrice float64 `json:"unit_price"` // Price per unit UnitPrice float64 `json:"unit_price"` // Price per unit
TotalPrice float64 `json:"total_price"` // Total price for this item (Quantity * UnitPrice) 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"`
}

View File

@ -9,7 +9,7 @@ type OrderInquiryResponse struct {
ID string `json:"id"` ID string `json:"id"`
Status string `json:"status"` Status string `json:"status"`
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Fee float64 `json:"fee"` Tax float64 `json:"tax"`
Total float64 `json:"total"` Total float64 `json:"total"`
CustomerID int64 `json:"customer_id"` CustomerID int64 `json:"customer_id"`
PaymentType string `json:"payment_type"` PaymentType string `json:"payment_type"`
@ -54,7 +54,7 @@ func MapToInquiryResponse(result *entity.OrderInquiryResponse) OrderInquiryRespo
ID: result.OrderInquiry.ID, ID: result.OrderInquiry.ID,
Status: result.OrderInquiry.Status, Status: result.OrderInquiry.Status,
Amount: result.OrderInquiry.Amount, Amount: result.OrderInquiry.Amount,
Fee: result.OrderInquiry.Fee, Tax: result.OrderInquiry.Tax,
Total: result.OrderInquiry.Total, Total: result.OrderInquiry.Total,
CustomerID: result.OrderInquiry.CustomerID, CustomerID: result.OrderInquiry.CustomerID,
PaymentType: result.OrderInquiry.PaymentType, PaymentType: result.OrderInquiry.PaymentType,
@ -74,7 +74,7 @@ type OrderResponse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Status string `json:"status"` Status string `json:"status"`
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Fee float64 `json:"fee"` Tax float64 `json:"tax"`
Total float64 `json:"total"` Total float64 `json:"total"`
CustomerName string `json:"customer_name,omitempty"` CustomerName string `json:"customer_name,omitempty"`
PaymentType string `json:"payment_type"` PaymentType string `json:"payment_type"`
@ -89,7 +89,7 @@ func MapToOrderResponse(result *entity.OrderResponse) OrderResponse {
ID: result.Order.ID, ID: result.Order.ID,
Status: result.Order.Status, Status: result.Order.Status,
Amount: result.Order.Amount, Amount: result.Order.Amount,
Fee: result.Order.Fee, Tax: result.Order.Tax,
Total: result.Order.Total, Total: result.Order.Total,
PaymentType: result.Order.PaymentType, PaymentType: result.Order.PaymentType,
CreatedAt: result.Order.CreatedAt, CreatedAt: result.Order.CreatedAt,

View File

@ -3,5 +3,5 @@ package response
type PagingMeta struct { type PagingMeta struct {
Page int `json:"page"` Page int `json:"page"`
Limit int `json:"limit"` Limit int `json:"limit"`
Total int64 `json:"total_data"` Total int64 `json:"total"`
} }

View File

@ -2,7 +2,6 @@ package repository
import ( import (
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/constants"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository/models" "enaklo-pos-be/internal/repository/models"
"github.com/pkg/errors" "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 { func (r *inprogressOrderRepository) toInProgressOrderDBModel(order *entity.InProgressOrder) models.InProgressOrderDB {
now := time2.Now() now := time2.Now()
return models.InProgressOrderDB{ return models.InProgressOrderDB{
ID: constants.GenerateUUID(), ID: order.ID,
PartnerID: order.PartnerID, PartnerID: order.PartnerID,
CustomerID: order.CustomerID, CustomerID: order.CustomerID,
CustomerName: order.CustomerName, CustomerName: order.CustomerName,
@ -207,7 +206,7 @@ func (r *inprogressOrderRepository) toOrderInquiryDBModel(inquiry *entity.OrderI
CustomerID: &inquiry.CustomerID, CustomerID: &inquiry.CustomerID,
Status: inquiry.Status, Status: inquiry.Status,
Amount: inquiry.Amount, Amount: inquiry.Amount,
Fee: inquiry.Fee, Tax: inquiry.Tax,
Total: inquiry.Total, Total: inquiry.Total,
PaymentType: inquiry.PaymentType, PaymentType: inquiry.PaymentType,
Source: inquiry.Source, Source: inquiry.Source,
@ -230,7 +229,7 @@ func (r *inprogressOrderRepository) toDomainOrderInquiryModel(dbModel *models.Or
PartnerID: dbModel.PartnerID, PartnerID: dbModel.PartnerID,
Status: dbModel.Status, Status: dbModel.Status,
Amount: dbModel.Amount, Amount: dbModel.Amount,
Fee: dbModel.Fee, Tax: dbModel.Tax,
Total: dbModel.Total, Total: dbModel.Total,
PaymentType: dbModel.PaymentType, PaymentType: dbModel.PaymentType,
Source: dbModel.Source, Source: dbModel.Source,

View File

@ -11,7 +11,7 @@ type OrderDB struct {
InquiryID *string `gorm:"column:inquiry_id"` InquiryID *string `gorm:"column:inquiry_id"`
Status string `gorm:"column:status"` Status string `gorm:"column:status"`
Amount float64 `gorm:"column:amount"` Amount float64 `gorm:"column:amount"`
Fee float64 `gorm:"column:fee"` Tax float64 `gorm:"column:tax"`
Total float64 `gorm:"column:total"` Total float64 `gorm:"column:total"`
PaymentType string `gorm:"column:payment_type"` PaymentType string `gorm:"column:payment_type"`
Source string `gorm:"column:source"` Source string `gorm:"column:source"`
@ -19,6 +19,10 @@ type OrderDB struct {
CreatedAt time.Time `gorm:"column:created_at"` CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"` UpdatedAt time.Time `gorm:"column:updated_at"`
OrderItems []OrderItemDB `gorm:"foreignKey:OrderID"` 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 { func (OrderDB) TableName() string {
@ -29,11 +33,13 @@ type OrderItemDB struct {
ID int64 `gorm:"primaryKey;column:order_item_id"` ID int64 `gorm:"primaryKey;column:order_item_id"`
OrderID int64 `gorm:"column:order_id"` OrderID int64 `gorm:"column:order_id"`
ItemID int64 `gorm:"column:item_id"` ItemID int64 `gorm:"column:item_id"`
ItemName string `gorm:"column:item_name"`
ItemType string `gorm:"column:item_type"` ItemType string `gorm:"column:item_type"`
Price float64 `gorm:"column:price"` Price float64 `gorm:"column:price"`
Quantity int `gorm:"column:quantity"` Quantity int `gorm:"column:quantity"`
CreatedBy int64 `gorm:"column:created_by"` CreatedBy int64 `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"column:created_at"` CreatedAt time.Time `gorm:"column:created_at"`
Product ProductDB `gorm:"foreignKey:ItemID;references:ID"`
} }
func (OrderItemDB) TableName() string { func (OrderItemDB) TableName() string {
@ -49,7 +55,7 @@ type OrderInquiryDB struct {
CustomerPhoneNumber string `gorm:"column:customer_phone_number"` CustomerPhoneNumber string `gorm:"column:customer_phone_number"`
Status string `gorm:"column:status"` Status string `gorm:"column:status"`
Amount float64 `gorm:"column:amount"` Amount float64 `gorm:"column:amount"`
Fee float64 `gorm:"column:fee"` Tax float64 `gorm:"column:tax"`
Total float64 `gorm:"column:total"` Total float64 `gorm:"column:total"`
PaymentType string `gorm:"column:payment_type"` PaymentType string `gorm:"column:payment_type"`
Source string `gorm:"column:source"` Source string `gorm:"column:source"`
@ -72,6 +78,7 @@ type InquiryItemDB struct {
InquiryID string `gorm:"column:inquiry_id"` InquiryID string `gorm:"column:inquiry_id"`
ItemID int64 `gorm:"column:item_id"` ItemID int64 `gorm:"column:item_id"`
ItemType string `gorm:"column:item_type"` ItemType string `gorm:"column:item_type"`
ItemName string `gorm:"column:item_name"`
Price float64 `gorm:"column:price"` Price float64 `gorm:"column:price"`
Quantity int `gorm:"column:quantity"` Quantity int `gorm:"column:quantity"`
CreatedBy int64 `gorm:"column:created_by"` CreatedBy int64 `gorm:"column:created_by"`

View File

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

View File

@ -17,6 +17,26 @@ type OrderRepository interface {
CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error)
FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error)
UpdateInquiryStatus(ctx mycontext.Context, id string, status string) 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 { type orderRepository struct {
@ -61,13 +81,13 @@ func (r *orderRepository) Create(ctx mycontext.Context, order *entity.Order) (*e
item.ID = itemDB.ID item.ID = itemDB.ID
} }
if order.InProgressOrderID != "" { if order.InProgressOrderID != 0 {
if err := tx.Where("in_progress_order_id = ?", order.InProgressOrderID).Delete(&models.InProgressOrderItemDB{}).Error; err != nil { if err := tx.Where("order_id = ?", order.InProgressOrderID).Delete(&models.OrderItemDB{}).Error; err != nil {
tx.Rollback() tx.Rollback()
return nil, errors.Wrap(err, "failed to delete in-progress order items") 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() tx.Rollback()
return nil, errors.Wrap(err, "failed to delete in-progress order") 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, InquiryID: inquiryDB.ID,
ItemID: item.ItemID, ItemID: item.ItemID,
ItemType: item.ItemType, ItemType: item.ItemType,
ItemName: item.ItemName,
Price: item.Price, Price: item.Price,
Quantity: item.Quantity, Quantity: item.Quantity,
CreatedBy: item.CreatedBy, CreatedBy: item.CreatedBy,
@ -162,6 +183,7 @@ func (r *orderRepository) FindInquiryByID(ctx mycontext.Context, id string) (*en
orderItems = append(orderItems, entity.OrderItem{ orderItems = append(orderItems, entity.OrderItem{
ItemID: itemDB.ItemID, ItemID: itemDB.ItemID,
ItemType: itemDB.ItemType, ItemType: itemDB.ItemType,
ItemName: itemDB.ItemName,
Price: itemDB.Price, Price: itemDB.Price,
Quantity: itemDB.Quantity, Quantity: itemDB.Quantity,
CreatedBy: itemDB.CreatedBy, CreatedBy: itemDB.CreatedBy,
@ -202,13 +224,17 @@ func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB {
InquiryID: order.InquiryID, InquiryID: order.InquiryID,
Status: order.Status, Status: order.Status,
Amount: order.Amount, Amount: order.Amount,
Fee: order.Fee, Tax: order.Tax,
Total: order.Total, Total: order.Total,
PaymentType: order.PaymentType, PaymentType: order.PaymentType,
Source: order.Source, Source: order.Source,
CreatedBy: order.CreatedBy, CreatedBy: order.CreatedBy,
CreatedAt: order.CreatedAt, CreatedAt: order.CreatedAt,
UpdatedAt: order.UpdatedAt, UpdatedAt: order.UpdatedAt,
OrderType: order.OrderType,
TableNumber: order.TableNumber,
PaymentProvider: order.PaymentProvider,
CustomerName: order.CustomerName,
} }
} }
@ -220,7 +246,7 @@ func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Or
InquiryID: dbModel.InquiryID, InquiryID: dbModel.InquiryID,
Status: dbModel.Status, Status: dbModel.Status,
Amount: dbModel.Amount, Amount: dbModel.Amount,
Fee: dbModel.Fee, Tax: dbModel.Tax,
Total: dbModel.Total, Total: dbModel.Total,
PaymentType: dbModel.PaymentType, PaymentType: dbModel.PaymentType,
Source: dbModel.Source, Source: dbModel.Source,
@ -228,6 +254,10 @@ func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Or
CreatedAt: dbModel.CreatedAt, CreatedAt: dbModel.CreatedAt,
UpdatedAt: dbModel.UpdatedAt, UpdatedAt: dbModel.UpdatedAt,
OrderItems: []entity.OrderItem{}, 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, OrderID: item.OrderID,
ItemID: item.ItemID, ItemID: item.ItemID,
ItemType: item.ItemType, ItemType: item.ItemType,
ItemName: item.ItemName,
Price: item.Price, Price: item.Price,
Quantity: item.Quantity, Quantity: item.Quantity,
CreatedBy: item.CreatedBy, CreatedBy: item.CreatedBy,
@ -254,6 +285,7 @@ func (r *orderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *e
Quantity: dbModel.Quantity, Quantity: dbModel.Quantity,
CreatedBy: dbModel.CreatedBy, CreatedBy: dbModel.CreatedBy,
CreatedAt: dbModel.CreatedAt, CreatedAt: dbModel.CreatedAt,
ItemName: dbModel.ItemName,
} }
} }
@ -264,7 +296,7 @@ func (r *orderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) mo
CustomerID: &inquiry.CustomerID, CustomerID: &inquiry.CustomerID,
Status: inquiry.Status, Status: inquiry.Status,
Amount: inquiry.Amount, Amount: inquiry.Amount,
Fee: inquiry.Fee, Tax: inquiry.Tax,
Total: inquiry.Total, Total: inquiry.Total,
PaymentType: inquiry.PaymentType, PaymentType: inquiry.PaymentType,
Source: inquiry.Source, Source: inquiry.Source,
@ -287,7 +319,7 @@ func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiry
PartnerID: dbModel.PartnerID, PartnerID: dbModel.PartnerID,
Status: dbModel.Status, Status: dbModel.Status,
Amount: dbModel.Amount, Amount: dbModel.Amount,
Fee: dbModel.Fee, Tax: dbModel.Tax,
Total: dbModel.Total, Total: dbModel.Total,
PaymentType: dbModel.PaymentType, PaymentType: dbModel.PaymentType,
Source: dbModel.Source, Source: dbModel.Source,
@ -295,6 +327,10 @@ func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiry
CreatedAt: dbModel.CreatedAt, CreatedAt: dbModel.CreatedAt,
ExpiresAt: dbModel.ExpiresAt, ExpiresAt: dbModel.ExpiresAt,
OrderItems: []entity.OrderItem{}, OrderItems: []entity.OrderItem{},
OrderType: dbModel.OrderType,
CustomerName: dbModel.CustomerName,
PaymentProvider: dbModel.PaymentProvider,
TableNumber: dbModel.TableNumber,
} }
if dbModel.CustomerID != nil { if dbModel.CustomerID != nil {
@ -305,3 +341,486 @@ func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiry
return inquiry 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
}

View File

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

View File

@ -58,6 +58,7 @@ type RepoManagerImpl struct {
ProductRepo ProductRepository ProductRepo ProductRepository
TransactionRepo TransactionRepo TransactionRepo TransactionRepo
MemberRepository MemberRepository MemberRepository MemberRepository
PartnerSetting PartnerSettingsRepository
} }
func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { 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), TransactionRepo: NewTransactionRepository(db),
MemberRepository: NewMemberRepository(db), MemberRepository: NewMemberRepository(db),
InProgressOrderRepo: NewInProgressOrderRepository(db), InProgressOrderRepo: NewInProgressOrderRepository(db),
PartnerSetting: NewPartnerSettingsRepository(db),
} }
} }

View File

@ -2,7 +2,6 @@ package routes
import ( import (
"enaklo-pos-be/internal/handlers/http/customerauth" "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/handlers/http/discovery"
"enaklo-pos-be/internal/middlewares" "enaklo-pos-be/internal/middlewares"
@ -20,7 +19,6 @@ func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceMan
serverRoutes := []HTTPHandlerRoutes{ serverRoutes := []HTTPHandlerRoutes{
discovery.NewHandler(serviceManager.DiscoverService), discovery.NewHandler(serviceManager.DiscoverService),
customerauth.NewAuthHandler(serviceManager.AuthSvc, serviceManager.UserSvc, serviceManager.CustomerV2Svc), customerauth.NewAuthHandler(serviceManager.AuthSvc, serviceManager.UserSvc, serviceManager.CustomerV2Svc),
customerorder.NewHandler(serviceManager.OrderSvc),
} }
for _, handler := range serverRoutes { for _, handler := range serverRoutes {

View File

@ -4,9 +4,6 @@ import (
http2 "enaklo-pos-be/internal/handlers/http" http2 "enaklo-pos-be/internal/handlers/http"
"enaklo-pos-be/internal/handlers/http/balance" "enaklo-pos-be/internal/handlers/http/balance"
"enaklo-pos-be/internal/handlers/http/license" "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/oss"
"enaklo-pos-be/internal/handlers/http/partner" "enaklo-pos-be/internal/handlers/http/partner"
"enaklo-pos-be/internal/handlers/http/product" "enaklo-pos-be/internal/handlers/http/product"
@ -54,15 +51,12 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana
user.NewHandler(serviceManager.UserSvc), user.NewHandler(serviceManager.UserSvc),
studio.NewStudioHandler(serviceManager.StudioSvc), studio.NewStudioHandler(serviceManager.StudioSvc),
product.NewHandler(serviceManager.ProductSvc), product.NewHandler(serviceManager.ProductSvc),
order.NewHandler(serviceManager.OrderSvc),
oss.NewOssHandler(serviceManager.OSSSvc), oss.NewOssHandler(serviceManager.OSSSvc),
partner.NewHandler(serviceManager.PartnerSvc), partner.NewHandler(serviceManager.PartnerSvc),
site.NewHandler(serviceManager.SiteSvc), site.NewHandler(serviceManager.SiteSvc),
mdtrns.NewHandler(serviceManager.OrderSvc),
license.NewHandler(serviceManager.LicenseSvc), license.NewHandler(serviceManager.LicenseSvc),
transaction.New(serviceManager.Transaction), transaction.New(serviceManager.Transaction),
balance.NewHandler(serviceManager.Balance), balance.NewHandler(serviceManager.Balance),
linkqu.NewHandler(serviceManager.OrderSvc),
} }
for _, handler := range serverRoutes { for _, handler := range serverRoutes {

View File

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

View File

@ -7,7 +7,6 @@ import (
"enaklo-pos-be/internal/services/discovery" "enaklo-pos-be/internal/services/discovery"
service "enaklo-pos-be/internal/services/license" service "enaklo-pos-be/internal/services/license"
"enaklo-pos-be/internal/services/member" "enaklo-pos-be/internal/services/member"
"enaklo-pos-be/internal/services/order"
"enaklo-pos-be/internal/services/oss" "enaklo-pos-be/internal/services/oss"
"enaklo-pos-be/internal/services/partner" "enaklo-pos-be/internal/services/partner"
"enaklo-pos-be/internal/services/product" "enaklo-pos-be/internal/services/product"
@ -18,6 +17,7 @@ import (
customerSvc "enaklo-pos-be/internal/services/v2/customer" customerSvc "enaklo-pos-be/internal/services/v2/customer"
"enaklo-pos-be/internal/services/v2/inprogress_order" "enaklo-pos-be/internal/services/v2/inprogress_order"
orderSvc "enaklo-pos-be/internal/services/v2/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" productSvc "enaklo-pos-be/internal/services/v2/product"
"gorm.io/gorm" "gorm.io/gorm"
@ -35,7 +35,6 @@ type ServiceManagerImpl struct {
UserSvc User UserSvc User
StudioSvc Studio StudioSvc Studio
ProductSvc Product ProductSvc Product
OrderSvc Order
OSSSvc OSSService OSSSvc OSSService
PartnerSvc Partner PartnerSvc Partner
SiteSvc Site SiteSvc Site
@ -55,15 +54,16 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
custSvcV2 := customerSvc.New(repo.CustomerRepo, repo.EmailService) custSvcV2 := customerSvc.New(repo.CustomerRepo, repo.EmailService)
productSvcV2 := productSvc.New(repo.ProductRepo) 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{ return &ServiceManagerImpl{
AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License), AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License),
EventSvc: event.NewEventService(repo.Event), EventSvc: event.NewEventService(repo.Event),
UserSvc: users.NewUserService(repo.User), UserSvc: users.NewUserService(repo.User),
StudioSvc: studio.NewStudioService(repo.Studio), StudioSvc: studio.NewStudioService(repo.Studio),
ProductSvc: product.NewProductService(repo.Product), 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), OSSSvc: oss.NewOSSService(repo.OSS),
PartnerSvc: partner.NewPartnerService( PartnerSvc: partner.NewPartnerService(
repo.Partner, users.NewUserService(repo.User), repo.Trx, repo.Wallet, repo.User), 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), Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx),
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction), Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery, repo.Product), 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), MemberRegistrationSvc: member.NewMemberRegistrationService(repo.MemberRepository, repo.EmailService, custSvcV2),
CustomerV2Svc: custSvcV2, CustomerV2Svc: custSvcV2,
InProgressSvc: inprogressOrder, InProgressSvc: inprogressOrder,

View File

@ -3,28 +3,98 @@ package inprogress_order
import ( import (
"enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
order2 "enaklo-pos-be/internal/constants/order"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository" "enaklo-pos-be/internal/services/v2/order"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
) )
type InProgressOrderService interface { type InProgressOrderService interface {
Save(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) Save(ctx mycontext.Context, order *entity.OrderRequest) (*entity.Order, error)
GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, 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 { 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{ 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) createdOrder, err := s.repo.CreateOrUpdate(ctx, order)
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("failed to create in-progress order", 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 return createdOrder, nil
} }
func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) { func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) {
orders, err := s.repo.GetListByPartnerID(ctx, partnerID, limit, offset) orders, err := s.repo.GetListByPartnerID(ctx, partnerID, limit, offset, order2.Pending.String())
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID", logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID",
zap.Error(err), zap.Error(err),

View File

@ -7,11 +7,12 @@ import (
"enaklo-pos-be/internal/constants" "enaklo-pos-be/internal/constants"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"go.uber.org/zap" "go.uber.org/zap"
"math"
) )
func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context, func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -23,7 +24,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -43,7 +44,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
req.PartnerID, req.PartnerID,
customerID, customerID,
orderCalculation.Subtotal, orderCalculation.Subtotal,
orderCalculation.Fee, orderCalculation.Tax,
orderCalculation.Total, orderCalculation.Total,
req.PaymentMethod, req.PaymentMethod,
req.Source, req.Source,
@ -79,7 +80,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
}, nil }, 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 productIDs []int64
var filteredItems []entity.OrderItemRequest var filteredItems []entity.OrderItemRequest
@ -98,7 +99,7 @@ func (s *orderSvc) validateOrderItems(ctx mycontext.Context, items []entity.Orde
return productIDs, filteredItems, nil return productIDs, filteredItems, nil
} }
func (s *orderSvc) calculateOrderTotals( func (s *orderSvc) CalculateOrderTotals(
ctx mycontext.Context, ctx mycontext.Context,
items []entity.OrderItemRequest, items []entity.OrderItemRequest,
productDetails *entity.ProductDetails, productDetails *entity.ProductDetails,
@ -114,12 +115,23 @@ func (s *orderSvc) calculateOrderTotals(
subtotal += product.Price * float64(item.Quantity) 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{ return &entity.OrderCalculation{
Subtotal: subtotal, Subtotal: subtotal,
Fee: fee, Tax: tax,
Total: subtotal + fee, Total: subtotal + tax,
}, nil }, nil
} }
@ -145,3 +157,79 @@ func (s *orderSvc) validateInquiry(ctx mycontext.Context, token string) (*entity
return inquiry, nil 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)
}

View File

@ -10,7 +10,7 @@ import (
) )
func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context, 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) inquiry, err := s.validateInquiry(ctx, token)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -12,6 +12,24 @@ type Repository interface {
CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error)
FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error)
UpdateInquiryStatus(ctx mycontext.Context, id string, status string) 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 { type ProductService interface {
@ -42,13 +60,55 @@ type Service interface {
CreateOrderInquiry(ctx mycontext.Context, CreateOrderInquiry(ctx mycontext.Context,
req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) req *entity.OrderRequest) (*entity.OrderInquiryResponse, error)
ExecuteOrderInquiry(ctx mycontext.Context, 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 { type Config interface {
GetOrderFee(source string) float64 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 { type orderSvc struct {
repo Repository repo Repository
product ProductService product ProductService
@ -57,6 +117,8 @@ type orderSvc struct {
crypt CryptService crypt CryptService
cfg Config cfg Config
notification NotificationService notification NotificationService
partnerSetting PartnerSettings
inprogressOrder InProgressOrderRepository
} }
func New( func New(
@ -67,6 +129,7 @@ func New(
crypt CryptService, crypt CryptService,
cfg Config, cfg Config,
notification NotificationService, notification NotificationService,
partnerSetting PartnerSettings,
) Service { ) Service {
return &orderSvc{ return &orderSvc{
repo: repo, repo: repo,
@ -76,5 +139,6 @@ func New(
crypt: crypt, crypt: crypt,
cfg: cfg, cfg: cfg,
notification: notification, notification: notification,
partnerSetting: partnerSetting,
} }
} }

View File

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

View File

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