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

@ -3,9 +3,10 @@ package order
type OrderStatus string type OrderStatus string
const ( 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

@ -5,20 +5,24 @@ import (
) )
type OrderDB struct { type OrderDB struct {
ID int64 `gorm:"primaryKey;column:id"` ID int64 `gorm:"primaryKey;column:id"`
PartnerID int64 `gorm:"column:partner_id"` PartnerID int64 `gorm:"column:partner_id"`
CustomerID *int64 `gorm:"column:customer_id"` CustomerID *int64 `gorm:"column:customer_id"`
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"`
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"`
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,
@ -196,38 +218,46 @@ func (r *orderRepository) UpdateInquiryStatus(ctx mycontext.Context, id string,
func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB { func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB {
return models.OrderDB{ return models.OrderDB{
ID: order.ID, ID: order.ID,
PartnerID: order.PartnerID, PartnerID: order.PartnerID,
CustomerID: order.CustomerID, CustomerID: order.CustomerID,
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,
} }
} }
func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order { func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order {
return &entity.Order{ return &entity.Order{
ID: dbModel.ID, ID: dbModel.ID,
PartnerID: dbModel.PartnerID, PartnerID: dbModel.PartnerID,
CustomerID: dbModel.CustomerID, CustomerID: dbModel.CustomerID,
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,
CreatedBy: dbModel.CreatedBy, CreatedBy: dbModel.CreatedBy,
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,
@ -283,18 +315,22 @@ func (r *orderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) mo
func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiryDB) *entity.OrderInquiry { func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiryDB) *entity.OrderInquiry {
inquiry := &entity.OrderInquiry{ inquiry := &entity.OrderInquiry{
ID: dbModel.ID, ID: dbModel.ID,
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,
CreatedBy: dbModel.CreatedBy, CreatedBy: dbModel.CreatedBy,
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,21 +60,65 @@ 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
customer CustomerService customer CustomerService
transaction TransactionService transaction TransactionService
crypt CryptService crypt CryptService
cfg Config cfg Config
notification NotificationService notification NotificationService
partnerSetting PartnerSettings
inprogressOrder InProgressOrderRepository
} }
func New( func New(
@ -67,14 +129,16 @@ 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,
product: product, product: product,
customer: customer, customer: customer,
transaction: transaction, transaction: transaction,
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,
}
}