upodate system
This commit is contained in:
parent
1201b2e45b
commit
f31f83e485
@ -4,6 +4,7 @@ import "time"
|
|||||||
|
|
||||||
type CashierSession struct {
|
type CashierSession struct {
|
||||||
ID int64
|
ID int64
|
||||||
|
PartnerID int64
|
||||||
CashierID int64
|
CashierID int64
|
||||||
OpenedAt time.Time
|
OpenedAt time.Time
|
||||||
ClosedAt *time.Time
|
ClosedAt *time.Time
|
||||||
|
|||||||
@ -86,13 +86,13 @@ type OrderItem struct {
|
|||||||
ItemType string `gorm:"type:varchar;column:item_type"`
|
ItemType string `gorm:"type:varchar;column:item_type"`
|
||||||
Price float64 `gorm:"type:numeric;not null;column:price"`
|
Price float64 `gorm:"type:numeric;not null;column:price"`
|
||||||
Quantity int `gorm:"type:int;column:quantity"`
|
Quantity int `gorm:"type:int;column:quantity"`
|
||||||
|
Status string `gorm:"type:varchar;column:status;default:ACTIVE"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
||||||
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"`
|
ItemName string `gorm:"type:varchar;column:item_name"`
|
||||||
Description string `gorm:"type:varchar;column:description"`
|
|
||||||
Notes string `gorm:"type:varchar;column:notes"`
|
Notes string `gorm:"type:varchar;column:notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,9 +5,10 @@ import (
|
|||||||
"enaklo-pos-be/internal/handlers/request"
|
"enaklo-pos-be/internal/handlers/request"
|
||||||
"enaklo-pos-be/internal/handlers/response"
|
"enaklo-pos-be/internal/handlers/response"
|
||||||
"enaklo-pos-be/internal/services/v2/cashier_session"
|
"enaklo-pos-be/internal/services/v2/cashier_session"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CashierSessionHandler struct {
|
type CashierSessionHandler struct {
|
||||||
@ -15,7 +16,9 @@ type CashierSessionHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewCashierSession(service cashier_session.Service) *CashierSessionHandler {
|
func NewCashierSession(service cashier_session.Service) *CashierSessionHandler {
|
||||||
return &CashierSessionHandler{service: service}
|
return &CashierSessionHandler{
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
||||||
@ -26,6 +29,7 @@ func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFun
|
|||||||
route.POST("/close/:id", h.CloseSession)
|
route.POST("/close/:id", h.CloseSession)
|
||||||
route.GET("/open", h.GetOpenSession)
|
route.GET("/open", h.GetOpenSession)
|
||||||
route.GET("/report/:id", h.GetSessionReport)
|
route.GET("/report/:id", h.GetSessionReport)
|
||||||
|
route.GET("/history", h.GetSessionHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CashierSessionHandler) OpenSession(c *gin.Context) {
|
func (h *CashierSessionHandler) OpenSession(c *gin.Context) {
|
||||||
@ -121,3 +125,45 @@ func (h *CashierSessionHandler) GetSessionReport(c *gin.Context) {
|
|||||||
Data: response.MapToCashierSessionReport(report),
|
Data: response.MapToCashierSessionReport(report),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *CashierSessionHandler) GetSessionHistory(c *gin.Context) {
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
partnerID := ctx.GetPartnerID()
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
limitStr := c.DefaultQuery("limit", "10")
|
||||||
|
offsetStr := c.DefaultQuery("offset", "0")
|
||||||
|
|
||||||
|
limit, err := strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit < 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
if limit > 50 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, err := strconv.Atoi(offsetStr)
|
||||||
|
if err != nil || offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions, total, err := h.service.GetSessionHistory(ctx, *partnerID, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := make([]*response.CashierSessionResponse, len(sessions))
|
||||||
|
for i, session := range sessions {
|
||||||
|
responseData[i] = response.MapToCashierSessionResponse(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
pagingMeta := response.NewPaginationHelper().BuildPagingMeta(offset, limit, total)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: responseData,
|
||||||
|
PagingMeta: pagingMeta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -27,7 +27,6 @@ func (h *CustomerOrderHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc
|
|||||||
|
|
||||||
route.GET("/history", jwt, h.GetOrderHistory)
|
route.GET("/history", jwt, h.GetOrderHistory)
|
||||||
route.GET("/detail/:id", jwt, h.GetOrderID)
|
route.GET("/detail/:id", jwt, h.GetOrderID)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
|
func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
|
||||||
@ -99,6 +98,7 @@ func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
|
|||||||
Price: item.Price,
|
Price: item.Price,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
Subtotal: item.Price * float64(item.Quantity),
|
Subtotal: item.Price * float64(item.Quantity),
|
||||||
|
Status: item.Status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,20 +7,23 @@ import (
|
|||||||
"enaklo-pos-be/internal/handlers/request"
|
"enaklo-pos-be/internal/handlers/request"
|
||||||
"enaklo-pos-be/internal/handlers/response"
|
"enaklo-pos-be/internal/handlers/response"
|
||||||
"enaklo-pos-be/internal/services/v2/order"
|
"enaklo-pos-be/internal/services/v2/order"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
service order.Service
|
service order.Service
|
||||||
|
queryParser *request.QueryParser
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOrderHandler(service order.Service) *Handler {
|
func NewOrderHandler(service order.Service) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
service: service,
|
service: service,
|
||||||
|
queryParser: request.NewQueryParser(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +42,7 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
|||||||
route.GET("/revenue-overview", jwt, h.GetRevenueOverview)
|
route.GET("/revenue-overview", jwt, h.GetRevenueOverview)
|
||||||
route.GET("/sales-by-category", jwt, h.GetSalesByCategory)
|
route.GET("/sales-by-category", jwt, h.GetSalesByCategory)
|
||||||
route.GET("/popular-products", jwt, h.GetPopularProducts)
|
route.GET("/popular-products", jwt, h.GetPopularProducts)
|
||||||
|
route.GET("/detail/:id", jwt, h.GetByID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type InquiryRequest struct {
|
type InquiryRequest struct {
|
||||||
@ -106,8 +110,6 @@ type VoidItemRequest struct {
|
|||||||
type SplitBillRequest struct {
|
type SplitBillRequest struct {
|
||||||
OrderID int64 `json:"order_id" validate:"required"`
|
OrderID int64 `json:"order_id" validate:"required"`
|
||||||
Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"`
|
Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"`
|
||||||
PaymentMethod string `json:"payment_method" validate:"required"`
|
|
||||||
PaymentProvider string `json:"payment_provider"`
|
|
||||||
Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
|
Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
|
||||||
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
|
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
|
||||||
}
|
}
|
||||||
@ -318,7 +320,7 @@ func (h *Handler) Refund(c *gin.Context) {
|
|||||||
Reason: req.Reason,
|
Reason: req.Reason,
|
||||||
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
CustomerName: order.CustomerName,
|
CustomerName: order.CustomerName,
|
||||||
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response.BaseResponse{
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
@ -330,116 +332,30 @@ func (h *Handler) Refund(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *Handler) GetOrderHistory(c *gin.Context) {
|
func (h *Handler) GetOrderHistory(c *gin.Context) {
|
||||||
ctx := request.GetMyContext(c)
|
ctx := request.GetMyContext(c)
|
||||||
partnerID := ctx.GetPartnerID()
|
|
||||||
|
|
||||||
limitStr := c.Query("limit")
|
searchReq, err := h.queryParser.ParseSearchRequest(c)
|
||||||
offsetStr := c.Query("offset")
|
|
||||||
status := c.Query("status")
|
|
||||||
startDateStr := c.Query("start_date")
|
|
||||||
endDateStr := c.Query("end_date")
|
|
||||||
|
|
||||||
searchReq := entity.SearchRequest{}
|
|
||||||
|
|
||||||
if status != "" {
|
|
||||||
searchReq.Status = status
|
|
||||||
}
|
|
||||||
|
|
||||||
limit := 20
|
|
||||||
if limitStr != "" {
|
|
||||||
parsedLimit, err := strconv.Atoi(limitStr)
|
|
||||||
if err == nil && parsedLimit > 0 {
|
|
||||||
limit = parsedLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if limit > 100 {
|
|
||||||
limit = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
searchReq.Limit = limit
|
|
||||||
|
|
||||||
offset := 0
|
|
||||||
|
|
||||||
if offsetStr != "" {
|
|
||||||
parsedOffset, err := strconv.Atoi(offsetStr)
|
|
||||||
if err == nil && parsedOffset >= 0 {
|
|
||||||
offset = parsedOffset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchReq.Offset = offset
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
response.ErrorWrapper(c, err)
|
response.ErrorWrapper(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData := []response.OrderHistoryResponse{}
|
orders, total, err := h.service.GetOrderHistory(ctx, *searchReq)
|
||||||
for _, order := range orders {
|
if err != nil {
|
||||||
var orderItems []response.OrderItemResponse
|
response.ErrorWrapper(c, err)
|
||||||
for _, item := range order.OrderItems {
|
return
|
||||||
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{
|
responseData := response.MapOrderHistoryResponse(orders)
|
||||||
ID: order.ID,
|
pagingMeta := response.NewPaginationHelper().BuildPagingMeta(searchReq.Offset, searchReq.Limit, total)
|
||||||
CustomerName: order.CustomerName,
|
|
||||||
CustomerID: order.CustomerID,
|
|
||||||
IsMember: order.IsMemberOrder(),
|
|
||||||
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{
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
Data: responseData,
|
Data: responseData,
|
||||||
PagingMeta: &response.PagingMeta{
|
PagingMeta: 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) {
|
func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) {
|
||||||
ctx := request.GetMyContext(c)
|
ctx := request.GetMyContext(c)
|
||||||
partnerID := ctx.GetPartnerID()
|
partnerID := ctx.GetPartnerID()
|
||||||
@ -501,7 +417,7 @@ func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) {
|
|||||||
paymentBreakdown := make([]PaymentMethodBreakdown, len(paymentAnalysis.PaymentMethodBreakdown))
|
paymentBreakdown := make([]PaymentMethodBreakdown, len(paymentAnalysis.PaymentMethodBreakdown))
|
||||||
for i, bd := range paymentAnalysis.PaymentMethodBreakdown {
|
for i, bd := range paymentAnalysis.PaymentMethodBreakdown {
|
||||||
paymentBreakdown[i] = PaymentMethodBreakdown{
|
paymentBreakdown[i] = PaymentMethodBreakdown{
|
||||||
PaymentMethod: h.formatPayment(bd.PaymentType, bd.PaymentProvider),
|
PaymentMethod: response.NewPaymentFormatter().Format(bd.PaymentType, bd.PaymentProvider),
|
||||||
TotalTransactions: bd.TotalTransactions,
|
TotalTransactions: bd.TotalTransactions,
|
||||||
TotalAmount: bd.TotalAmount,
|
TotalAmount: bd.TotalAmount,
|
||||||
}
|
}
|
||||||
@ -632,7 +548,6 @@ func (h *Handler) GetPopularProducts(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *Handler) GetRefundHistory(c *gin.Context) {
|
func (h *Handler) GetRefundHistory(c *gin.Context) {
|
||||||
ctx := request.GetMyContext(c)
|
ctx := request.GetMyContext(c)
|
||||||
partnerID := ctx.GetPartnerID()
|
|
||||||
|
|
||||||
limitStr := c.Query("limit")
|
limitStr := c.Query("limit")
|
||||||
offsetStr := c.Query("offset")
|
offsetStr := c.Query("offset")
|
||||||
@ -664,8 +579,6 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchReq.Offset = offset
|
searchReq.Offset = offset
|
||||||
|
|
||||||
// Set status to REFUNDED to get only refunded orders
|
|
||||||
searchReq.Status = "REFUNDED"
|
searchReq.Status = "REFUNDED"
|
||||||
|
|
||||||
if startDateStr != "" {
|
if startDateStr != "" {
|
||||||
@ -682,7 +595,7 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
orders, total, err := h.service.GetOrderHistory(ctx, *partnerID, searchReq)
|
orders, total, err := h.service.GetOrderHistory(ctx, searchReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorWrapper(c, err)
|
response.ErrorWrapper(c, err)
|
||||||
return
|
return
|
||||||
@ -698,7 +611,7 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
|
|||||||
Status: order.Status,
|
Status: order.Status,
|
||||||
Amount: order.Amount,
|
Amount: order.Amount,
|
||||||
Total: order.Total,
|
Total: order.Total,
|
||||||
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
|
||||||
TableNumber: order.TableNumber,
|
TableNumber: order.TableNumber,
|
||||||
OrderType: order.OrderType,
|
OrderType: order.OrderType,
|
||||||
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
@ -759,7 +672,6 @@ func (h *Handler) PartialRefund(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate refunded amount
|
|
||||||
refundedAmount := 0.0
|
refundedAmount := 0.0
|
||||||
var refundedItems []RefundedItemResponse
|
var refundedItems []RefundedItemResponse
|
||||||
|
|
||||||
@ -789,7 +701,7 @@ func (h *Handler) PartialRefund(c *gin.Context) {
|
|||||||
Reason: req.Reason,
|
Reason: req.Reason,
|
||||||
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
CustomerName: order.CustomerName,
|
CustomerName: order.CustomerName,
|
||||||
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
|
||||||
RefundedItems: refundedItems,
|
RefundedItems: refundedItems,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,7 +727,6 @@ func (h *Handler) VoidOrder(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert request items to entity items
|
|
||||||
var items []entity.VoidItem
|
var items []entity.VoidItem
|
||||||
if req.Type == "ITEM" {
|
if req.Type == "ITEM" {
|
||||||
items = make([]entity.VoidItem, len(req.Items))
|
items = make([]entity.VoidItem, len(req.Items))
|
||||||
@ -906,7 +817,8 @@ func (h *Handler) SplitBill(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
splitOrder, err := h.service.SplitBillRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Type, req.PaymentMethod, req.PaymentProvider, items, req.Amount)
|
splitOrder, err := h.service.SplitBillRequest(ctx,
|
||||||
|
*ctx.GetPartnerID(), req.OrderID, req.Type, items, req.Amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorWrapper(c, err)
|
response.ErrorWrapper(c, err)
|
||||||
return
|
return
|
||||||
@ -918,3 +830,26 @@ func (h *Handler) SplitBill(c *gin.Context) {
|
|||||||
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}),
|
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetByID(c *gin.Context) {
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
partnerID := ctx.GetPartnerID()
|
||||||
|
|
||||||
|
orderID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order, err := h.service.GetOrderByIDAndPartnerID(ctx, orderID, *partnerID)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: order}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package request
|
|||||||
import "enaklo-pos-be/internal/entity"
|
import "enaklo-pos-be/internal/entity"
|
||||||
|
|
||||||
type OpenCashierSessionRequest struct {
|
type OpenCashierSessionRequest struct {
|
||||||
|
PartnerID int64 `json:"partner_id" validate:"required"`
|
||||||
OpeningAmount float64 `json:"opening_amount" validate:"required,gt=0"`
|
OpeningAmount float64 `json:"opening_amount" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ type CloseCashierSessionRequest struct {
|
|||||||
|
|
||||||
func (o *OpenCashierSessionRequest) ToEntity(cashierID int64) *entity.CashierSession {
|
func (o *OpenCashierSessionRequest) ToEntity(cashierID int64) *entity.CashierSession {
|
||||||
return &entity.CashierSession{
|
return &entity.CashierSession{
|
||||||
|
PartnerID: o.PartnerID,
|
||||||
CashierID: cashierID,
|
CashierID: cashierID,
|
||||||
OpeningAmount: o.OpeningAmount,
|
OpeningAmount: o.OpeningAmount,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,8 +36,6 @@ type Product struct {
|
|||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Price float64 `json:"price" validate:"required"`
|
Price float64 `json:"price" validate:"required"`
|
||||||
IsWeekendTicket bool `json:"is_weekend_ticket"`
|
|
||||||
IsSeasonTicket bool `json:"is_season_ticket"`
|
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Stock int64 `json:"stock"`
|
Stock int64 `json:"stock"`
|
||||||
|
|||||||
126
internal/handlers/request/query.go
Normal file
126
internal/handlers/request/query.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"enaklo-pos-be/internal/entity"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueryParser struct {
|
||||||
|
defaultLimit int
|
||||||
|
maxLimit int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueryParser() *QueryParser {
|
||||||
|
return &QueryParser{
|
||||||
|
defaultLimit: 20,
|
||||||
|
maxLimit: 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QueryParser) ParseSearchRequest(c *gin.Context) (*entity.SearchRequest, error) {
|
||||||
|
req := &entity.SearchRequest{}
|
||||||
|
|
||||||
|
if status := c.Query("status"); status != "" {
|
||||||
|
req.Status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, err := p.parseLimit(c.Query("limit"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid limit parameter")
|
||||||
|
}
|
||||||
|
req.Limit = limit
|
||||||
|
|
||||||
|
offset, err := p.parseOffset(c.Query("offset"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid offset parameter")
|
||||||
|
}
|
||||||
|
req.Offset = offset
|
||||||
|
|
||||||
|
if err := p.parseDateRange(c, req); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid date parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QueryParser) parseLimit(limitStr string) (int, error) {
|
||||||
|
if limitStr == "" {
|
||||||
|
return p.defaultLimit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, err := strconv.Atoi(limitStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("limit must be a valid integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit <= 0 {
|
||||||
|
return p.defaultLimit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > p.maxLimit {
|
||||||
|
limit = p.maxLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
return limit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QueryParser) parseOffset(offsetStr string) (int, error) {
|
||||||
|
if offsetStr == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, err := strconv.Atoi(offsetStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("offset must be a valid integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset < 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QueryParser) parseDateRange(c *gin.Context, req *entity.SearchRequest) error {
|
||||||
|
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
||||||
|
startDate, err := p.parseDate(startDateStr)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid start_date format")
|
||||||
|
}
|
||||||
|
req.Start = startDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if endDateStr := c.Query("end_date"); endDateStr != "" {
|
||||||
|
endDate, err := p.parseDate(endDateStr)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid end_date format")
|
||||||
|
}
|
||||||
|
req.End = endDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.Start.IsZero() && !req.End.IsZero() && req.Start.After(req.End) {
|
||||||
|
return errors.New("start_date cannot be after end_date")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *QueryParser) parseDate(dateStr string) (time.Time, error) {
|
||||||
|
formats := []string{
|
||||||
|
time.RFC3339,
|
||||||
|
"2006-01-02T15:04:05",
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
if date, err := time.Parse(format, dateStr); err == nil {
|
||||||
|
return date, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, errors.New("unsupported date format")
|
||||||
|
}
|
||||||
43
internal/handlers/request/validator/request_validator.go
Normal file
43
internal/handlers/request/validator/request_validator.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"enaklo-pos-be/internal/entity"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestValidator struct{}
|
||||||
|
|
||||||
|
func NewRequestValidator() *RequestValidator {
|
||||||
|
return &RequestValidator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RequestValidator) ValidateSearchRequest(req *entity.SearchRequest) error {
|
||||||
|
if req.Status != "" {
|
||||||
|
validStatuses := []string{"pending", "confirmed", "processing", "completed", "cancelled"}
|
||||||
|
if !v.isValidStatus(req.Status, validStatuses) {
|
||||||
|
return errors.New("invalid status value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.Start.IsZero() && !req.End.IsZero() {
|
||||||
|
if req.Start.After(req.End) {
|
||||||
|
return errors.New("start date cannot be after end date")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.End.Sub(req.Start) > 365*24*time.Hour {
|
||||||
|
return errors.New("date range cannot exceed 1 year")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RequestValidator) isValidStatus(status string, validStatuses []string) bool {
|
||||||
|
for _, validStatus := range validStatuses {
|
||||||
|
if status == validStatus {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
type CashierSessionResponse struct {
|
type CashierSessionResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
|
PartnerID int64 `json:"partner_id"`
|
||||||
CashierID int64 `json:"cashier_id"`
|
CashierID int64 `json:"cashier_id"`
|
||||||
OpenedAt time.Time `json:"opened_at"`
|
OpenedAt time.Time `json:"opened_at"`
|
||||||
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
||||||
@ -35,6 +36,7 @@ func MapToCashierSessionResponse(e *entity.CashierSession) *CashierSessionRespon
|
|||||||
|
|
||||||
return &CashierSessionResponse{
|
return &CashierSessionResponse{
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
|
PartnerID: e.PartnerID,
|
||||||
CashierID: e.CashierID,
|
CashierID: e.CashierID,
|
||||||
OpenedAt: e.OpenedAt,
|
OpenedAt: e.OpenedAt,
|
||||||
ClosedAt: e.ClosedAt,
|
ClosedAt: e.ClosedAt,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package response
|
|||||||
import (
|
import (
|
||||||
"enaklo-pos-be/internal/constants/order"
|
"enaklo-pos-be/internal/constants/order"
|
||||||
"enaklo-pos-be/internal/constants/transaction"
|
"enaklo-pos-be/internal/constants/transaction"
|
||||||
|
"enaklo-pos-be/internal/entity"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -204,3 +205,52 @@ type OrderHistoryResponse struct {
|
|||||||
Tax float64 `json:"tax"`
|
Tax float64 `json:"tax"`
|
||||||
RestaurantName string `json:"restaurant_name"`
|
RestaurantName string `json:"restaurant_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MapOrderHistoryResponse(orders []*entity.Order) []OrderHistoryResponse {
|
||||||
|
responseData := make([]OrderHistoryResponse, 0, len(orders))
|
||||||
|
|
||||||
|
for _, order := range orders {
|
||||||
|
orderResponse := mapOrderToResponse(order)
|
||||||
|
responseData = append(responseData, orderResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapOrderToResponse(order *entity.Order) OrderHistoryResponse {
|
||||||
|
paymentFormatter := NewPaymentFormatter()
|
||||||
|
return OrderHistoryResponse{
|
||||||
|
ID: order.ID,
|
||||||
|
CustomerName: order.CustomerName,
|
||||||
|
CustomerID: order.CustomerID,
|
||||||
|
IsMember: order.IsMemberOrder(),
|
||||||
|
Status: order.Status,
|
||||||
|
Amount: order.Amount,
|
||||||
|
Total: order.Total,
|
||||||
|
PaymentType: paymentFormatter.Format(order.PaymentType, order.PaymentProvider),
|
||||||
|
TableNumber: order.TableNumber,
|
||||||
|
OrderType: order.OrderType,
|
||||||
|
OrderItems: mapOrderItems(order.OrderItems),
|
||||||
|
CreatedAt: order.CreatedAt.Format(time.RFC3339),
|
||||||
|
Tax: order.Tax,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapOrderItems(items []entity.OrderItem) []OrderItemResponse {
|
||||||
|
orderItems := make([]OrderItemResponse, 0, len(items))
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
orderItems = append(orderItems, OrderItemResponse{
|
||||||
|
OrderItemID: item.ID,
|
||||||
|
ProductID: item.ItemID,
|
||||||
|
ProductName: item.ItemName,
|
||||||
|
Price: item.Price,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
Subtotal: item.Price * float64(item.Quantity),
|
||||||
|
Notes: item.Notes,
|
||||||
|
Status: item.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderItems
|
||||||
|
}
|
||||||
|
|||||||
@ -23,12 +23,14 @@ type OrderInquiryResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OrderItemResponse struct {
|
type OrderItemResponse struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id"`
|
||||||
ProductID int64 `json:"product_id"`
|
ProductID int64 `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
Quantity int `json:"quantity"`
|
Quantity int `json:"quantity"`
|
||||||
Subtotal float64 `json:"subtotal"`
|
Subtotal float64 `json:"subtotal"`
|
||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
|
func mapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
|
||||||
@ -109,12 +111,14 @@ func MapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
|
|||||||
result := make([]OrderItemResponse, 0, len(items))
|
result := make([]OrderItemResponse, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
result = append(result, OrderItemResponse{
|
result = append(result, OrderItemResponse{
|
||||||
|
OrderItemID: item.ID,
|
||||||
ProductID: item.ItemID,
|
ProductID: item.ItemID,
|
||||||
ProductName: item.ItemName,
|
ProductName: item.ItemName,
|
||||||
Price: item.Price,
|
Price: item.Price,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
Subtotal: item.Price * float64(item.Quantity),
|
Subtotal: item.Price * float64(item.Quantity),
|
||||||
Notes: item.Notes,
|
Notes: item.Notes,
|
||||||
|
Status: item.Status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
34
internal/handlers/response/pagination_formatter.go
Normal file
34
internal/handlers/response/pagination_formatter.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
type PaginationHelper struct{}
|
||||||
|
|
||||||
|
func NewPaginationHelper() *PaginationHelper {
|
||||||
|
return &PaginationHelper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaginationHelper) BuildPagingMeta(offset, limit int, total int64) *PagingMeta {
|
||||||
|
page := 1
|
||||||
|
if limit > 0 {
|
||||||
|
page = (offset / limit) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PagingMeta{
|
||||||
|
Page: page,
|
||||||
|
Total: total,
|
||||||
|
Limit: limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaginationHelper) calculateTotalPages(total int64, limit int) int {
|
||||||
|
if limit <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return int((total + int64(limit) - 1) / int64(limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaginationHelper) hasNextPage(offset, limit int, total int64) bool {
|
||||||
|
if limit <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return int64(offset+limit) < total
|
||||||
|
}
|
||||||
20
internal/handlers/response/payment_formatter.go
Normal file
20
internal/handlers/response/payment_formatter.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type PaymentFormatter interface {
|
||||||
|
Format(paymentType, paymentProvider string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type paymentFormatter struct{}
|
||||||
|
|
||||||
|
func NewPaymentFormatter() PaymentFormatter {
|
||||||
|
return &paymentFormatter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *paymentFormatter) Format(paymentType, paymentProvider string) string {
|
||||||
|
if paymentProvider != "" {
|
||||||
|
return fmt.Sprintf("%s (%s)", paymentType, paymentProvider)
|
||||||
|
}
|
||||||
|
return paymentType
|
||||||
|
}
|
||||||
@ -1,112 +1,116 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"enaklo-pos-be/internal/common/logger"
|
||||||
"enaklo-pos-be/internal/common/mycontext"
|
"enaklo-pos-be/internal/common/mycontext"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
"enaklo-pos-be/internal/repository/models"
|
"enaklo-pos-be/internal/repository/models"
|
||||||
|
"enaklo-pos-be/internal/services/v2/inprogress_order"
|
||||||
|
time2 "time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
time2 "time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type InProgressOrderRepository interface {
|
type InProgressOrderRepository interface {
|
||||||
CreateOrUpdate(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error)
|
FindByID(ctx mycontext.Context, id int64) (*entity.Order, error)
|
||||||
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error)
|
CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error)
|
||||||
|
CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error
|
||||||
|
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
|
||||||
|
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type inprogressOrderRepository struct {
|
type inprogressOrderRepository struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInProgressOrderRepository(db *gorm.DB) *inprogressOrderRepository {
|
func NewInProgressOrderRepository(db *gorm.DB) inprogress_order.OrderRepository {
|
||||||
return &inprogressOrderRepository{db: db}
|
return &inprogressOrderRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) CreateOrUpdate(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) {
|
func (r *inprogressOrderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) {
|
||||||
isUpdate := order.ID != ""
|
var orderDB models.OrderDB
|
||||||
|
|
||||||
tx := r.db.Begin()
|
if err := r.db.Preload("OrderItems").First(&orderDB, id).Error; err != nil {
|
||||||
if tx.Error != nil {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, errors.Wrap(tx.Error, "failed to begin transaction")
|
return nil, errors.New("order not found")
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "failed to find order")
|
||||||
|
}
|
||||||
|
|
||||||
|
order := r.toDomainOrderModel(&orderDB)
|
||||||
|
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *inprogressOrderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
|
||||||
|
orderDB := r.toOrderDBModel(order)
|
||||||
|
|
||||||
|
// Use provided transaction or create new one
|
||||||
|
var dbTx *gorm.DB
|
||||||
|
if tx != nil {
|
||||||
|
dbTx = tx
|
||||||
|
} else {
|
||||||
|
dbTx = r.db.Begin()
|
||||||
|
if dbTx.Error != nil {
|
||||||
|
return nil, errors.Wrap(dbTx.Error, "failed to begin transaction")
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
tx.Rollback()
|
dbTx.Rollback()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
orderDB := r.toInProgressOrderDBModel(order)
|
|
||||||
|
|
||||||
if isUpdate {
|
|
||||||
var existingOrder models.InProgressOrderDB
|
|
||||||
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 {
|
if err := dbTx.Create(&orderDB).Error; err != nil {
|
||||||
tx.Rollback()
|
if tx == nil {
|
||||||
return nil, errors.Wrap(err, "failed to update order")
|
dbTx.Rollback()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Where("in_progress_order_id = ?", order.ID).Delete(&models.InProgressOrderItemDB{}).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")
|
return nil, errors.Wrap(err, "failed to insert order")
|
||||||
}
|
}
|
||||||
|
|
||||||
order.ID = orderDB.ID
|
order.ID = orderDB.ID
|
||||||
}
|
|
||||||
|
|
||||||
var itemIDs []int64
|
// Only commit if we created the transaction
|
||||||
for i := range order.OrderItems {
|
if tx == nil {
|
||||||
itemIDs = append(itemIDs, order.OrderItems[i].ItemID)
|
if err := dbTx.Commit().Error; err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
itemDB := r.toOrderItemDBModel(item, orderDB.ID)
|
|
||||||
|
|
||||||
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 nil, errors.Wrap(err, "failed to commit transaction")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) {
|
func (r *inprogressOrderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
|
||||||
var ordersDB []models.InProgressOrderDB
|
if len(items) == 0 {
|
||||||
query := r.db.Where("partner_id = ?", partnerID).Order("created_at DESC")
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsDB := make([]models.OrderItemDB, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
itemDB := r.toOrderItemDBModel(&item)
|
||||||
|
itemDB.OrderID = orderID
|
||||||
|
itemsDB[i] = itemDB
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&itemsDB).Error; err != nil {
|
||||||
|
return errors.Wrap(err, "failed to bulk insert order items")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range items {
|
||||||
|
items[i].ID = itemsDB[i].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (r *inprogressOrderRepository) 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 {
|
if limit > 0 {
|
||||||
query = query.Limit(limit)
|
query = query.Limit(limit)
|
||||||
@ -116,28 +120,40 @@ func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, pa
|
|||||||
query = query.Offset(offset)
|
query = query.Offset(offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := query.Preload("OrderItems.Product").Find(&ordersDB).Error; err != nil {
|
if err := query.Find(&ordersDB).Error; err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to find orders by partner ID")
|
return nil, errors.Wrap(err, "failed to find orders by partner ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
orders := make([]*entity.InProgressOrder, 0, len(ordersDB))
|
orders := make([]*entity.Order, 0, len(ordersDB))
|
||||||
for _, orderDB := range ordersDB {
|
for _, orderDB := range ordersDB {
|
||||||
order := r.toDomainOrderModel(&orderDB)
|
order := r.toDomainOrderModel(&orderDB)
|
||||||
order.OrderItems = make([]entity.InProgressOrderItem, 0, len(orderDB.OrderItems))
|
|
||||||
|
|
||||||
for _, itemDB := range orderDB.OrderItems {
|
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)
|
item := r.toDomainOrderItemModel(&itemDB)
|
||||||
|
|
||||||
orderItem := entity.InProgressOrderItem{
|
orderItem := entity.OrderItem{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
ItemID: item.ItemID,
|
ItemID: item.ItemID,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
|
ItemName: item.ItemName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if itemDB.Product.ID > 0 {
|
if itemDB.ItemID > 0 {
|
||||||
productDomain := r.toDomainProductModel(&itemDB.Product)
|
var product models.ProductDB
|
||||||
|
err := r.db.First(&product, itemDB.ItemID).Error
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
productDomain := r.toDomainProductModel(&product)
|
||||||
orderItem.Product = productDomain
|
orderItem.Product = productDomain
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
order.OrderItems = append(order.OrderItems, orderItem)
|
order.OrderItems = append(order.OrderItems, orderItem)
|
||||||
}
|
}
|
||||||
@ -148,85 +164,69 @@ func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, pa
|
|||||||
return orders, nil
|
return orders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) toInProgressOrderDBModel(order *entity.InProgressOrder) models.InProgressOrderDB {
|
func (r *inprogressOrderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) {
|
||||||
|
var orderDB models.OrderDB
|
||||||
|
|
||||||
|
if err := r.db.Preload("OrderItems").Where("id = ? AND partner_id = ?", id, partnerID).First(&orderDB).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.New("order not found")
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "failed to find order")
|
||||||
|
}
|
||||||
|
|
||||||
|
order := r.toDomainOrderModel(&orderDB)
|
||||||
|
|
||||||
|
for _, itemDB := range orderDB.OrderItems {
|
||||||
|
item := r.toDomainOrderItemModel(&itemDB)
|
||||||
|
order.OrderItems = append(order.OrderItems, *item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *inprogressOrderRepository) toOrderDBModel(order *entity.Order) models.OrderDB {
|
||||||
now := time2.Now()
|
now := time2.Now()
|
||||||
return models.InProgressOrderDB{
|
return models.OrderDB{
|
||||||
ID: order.ID,
|
ID: order.ID,
|
||||||
PartnerID: order.PartnerID,
|
PartnerID: order.PartnerID,
|
||||||
CustomerID: order.CustomerID,
|
CustomerID: order.CustomerID,
|
||||||
CustomerName: order.CustomerName,
|
CustomerName: order.CustomerName,
|
||||||
PaymentType: order.PaymentType,
|
PaymentType: order.PaymentType,
|
||||||
|
PaymentProvider: order.PaymentProvider,
|
||||||
CreatedBy: order.CreatedBy,
|
CreatedBy: order.CreatedBy,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
TableNumber: order.TableNumber,
|
TableNumber: order.TableNumber,
|
||||||
OrderType: order.OrderType,
|
OrderType: order.OrderType,
|
||||||
|
Status: order.Status,
|
||||||
|
Amount: order.Amount,
|
||||||
|
Total: order.Total,
|
||||||
|
Tax: order.Tax,
|
||||||
|
Source: order.Source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) toDomainOrderModel(dbModel *models.InProgressOrderDB) *entity.InProgressOrder {
|
func (r *inprogressOrderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order {
|
||||||
return &entity.InProgressOrder{
|
orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems))
|
||||||
|
for _, itemDB := range dbModel.OrderItems {
|
||||||
|
orderItems = append(orderItems, entity.OrderItem{
|
||||||
|
ItemID: itemDB.ItemID,
|
||||||
|
ItemType: itemDB.ItemType,
|
||||||
|
ItemName: itemDB.ItemName,
|
||||||
|
Price: itemDB.Price,
|
||||||
|
Quantity: itemDB.Quantity,
|
||||||
|
Status: itemDB.Status,
|
||||||
|
CreatedBy: itemDB.CreatedBy,
|
||||||
|
CreatedAt: itemDB.CreatedAt,
|
||||||
|
Notes: itemDB.Notes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entity.Order{
|
||||||
ID: dbModel.ID,
|
ID: dbModel.ID,
|
||||||
PartnerID: dbModel.PartnerID,
|
PartnerID: dbModel.PartnerID,
|
||||||
CustomerID: dbModel.CustomerID,
|
CustomerID: dbModel.CustomerID,
|
||||||
CustomerName: dbModel.CustomerName,
|
InquiryID: dbModel.InquiryID,
|
||||||
PaymentType: dbModel.PaymentType,
|
|
||||||
CreatedBy: dbModel.CreatedBy,
|
|
||||||
OrderItems: []entity.InProgressOrderItem{},
|
|
||||||
TableNumber: dbModel.TableNumber,
|
|
||||||
OrderType: dbModel.OrderType,
|
|
||||||
CreatedAt: dbModel.CreatedAt,
|
|
||||||
UpdatedAt: dbModel.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) toOrderItemDBModel(item *entity.InProgressOrderItem, inprogressOrderID string) models.InProgressOrderItemDB {
|
|
||||||
return models.InProgressOrderItemDB{
|
|
||||||
ID: item.ID,
|
|
||||||
InProgressOrderIO: inprogressOrderID,
|
|
||||||
ItemID: item.ItemID,
|
|
||||||
Quantity: item.Quantity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) toDomainOrderItemModel(dbModel *models.InProgressOrderItemDB) *entity.OrderItem {
|
|
||||||
return &entity.OrderItem{
|
|
||||||
ID: dbModel.ID,
|
|
||||||
ItemID: dbModel.ItemID,
|
|
||||||
Quantity: dbModel.Quantity,
|
|
||||||
CreatedBy: dbModel.CreatedBy,
|
|
||||||
CreatedAt: dbModel.CreatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) models.OrderInquiryDB {
|
|
||||||
return models.OrderInquiryDB{
|
|
||||||
ID: inquiry.ID,
|
|
||||||
PartnerID: inquiry.PartnerID,
|
|
||||||
CustomerID: &inquiry.CustomerID,
|
|
||||||
Status: inquiry.Status,
|
|
||||||
Amount: inquiry.Amount,
|
|
||||||
Tax: inquiry.Tax,
|
|
||||||
Total: inquiry.Total,
|
|
||||||
PaymentType: inquiry.PaymentType,
|
|
||||||
Source: inquiry.Source,
|
|
||||||
CreatedBy: inquiry.CreatedBy,
|
|
||||||
CreatedAt: inquiry.CreatedAt,
|
|
||||||
UpdatedAt: inquiry.UpdatedAt,
|
|
||||||
ExpiresAt: inquiry.ExpiresAt,
|
|
||||||
CustomerName: inquiry.CustomerName,
|
|
||||||
CustomerPhoneNumber: inquiry.CustomerPhoneNumber,
|
|
||||||
CustomerEmail: inquiry.CustomerEmail,
|
|
||||||
PaymentProvider: inquiry.PaymentProvider,
|
|
||||||
OrderType: inquiry.OrderType,
|
|
||||||
TableNumber: inquiry.TableNumber,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiryDB) *entity.OrderInquiry {
|
|
||||||
inquiry := &entity.OrderInquiry{
|
|
||||||
ID: dbModel.ID,
|
|
||||||
PartnerID: dbModel.PartnerID,
|
|
||||||
Status: dbModel.Status,
|
Status: dbModel.Status,
|
||||||
Amount: dbModel.Amount,
|
Amount: dbModel.Amount,
|
||||||
Tax: dbModel.Tax,
|
Tax: dbModel.Tax,
|
||||||
@ -235,17 +235,49 @@ func (r *inprogressOrderRepository) toDomainOrderInquiryModel(dbModel *models.Or
|
|||||||
Source: dbModel.Source,
|
Source: dbModel.Source,
|
||||||
CreatedBy: dbModel.CreatedBy,
|
CreatedBy: dbModel.CreatedBy,
|
||||||
CreatedAt: dbModel.CreatedAt,
|
CreatedAt: dbModel.CreatedAt,
|
||||||
ExpiresAt: dbModel.ExpiresAt,
|
UpdatedAt: dbModel.UpdatedAt,
|
||||||
OrderItems: []entity.OrderItem{},
|
OrderItems: orderItems,
|
||||||
|
CustomerName: dbModel.CustomerName,
|
||||||
|
TableNumber: dbModel.TableNumber,
|
||||||
|
OrderType: dbModel.OrderType,
|
||||||
|
PaymentProvider: dbModel.PaymentProvider,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if dbModel.CustomerID != nil {
|
func (r *inprogressOrderRepository) toOrderItemDBModel(item *entity.OrderItem) models.OrderItemDB {
|
||||||
inquiry.CustomerID = *dbModel.CustomerID
|
return models.OrderItemDB{
|
||||||
|
ID: item.ID,
|
||||||
|
OrderID: item.OrderID,
|
||||||
|
ItemID: item.ItemID,
|
||||||
|
ItemType: item.ItemType,
|
||||||
|
ItemName: item.ItemName,
|
||||||
|
Price: item.Price,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
Status: item.Status,
|
||||||
|
CreatedBy: item.CreatedBy,
|
||||||
|
CreatedAt: item.CreatedAt,
|
||||||
|
Notes: item.Notes,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inquiry.UpdatedAt = dbModel.UpdatedAt
|
func (r *inprogressOrderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *entity.OrderItem {
|
||||||
|
return &entity.OrderItem{
|
||||||
return inquiry
|
ID: dbModel.ID,
|
||||||
|
OrderID: dbModel.OrderID,
|
||||||
|
ItemID: dbModel.ItemID,
|
||||||
|
ItemType: dbModel.ItemType,
|
||||||
|
Price: dbModel.Price,
|
||||||
|
Quantity: dbModel.Quantity,
|
||||||
|
Status: dbModel.Status,
|
||||||
|
CreatedBy: dbModel.CreatedBy,
|
||||||
|
CreatedAt: dbModel.CreatedAt,
|
||||||
|
ItemName: dbModel.ItemName,
|
||||||
|
Notes: dbModel.Notes,
|
||||||
|
Product: &entity.Product{
|
||||||
|
ID: dbModel.ItemID,
|
||||||
|
Name: dbModel.ItemName,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product {
|
func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product {
|
||||||
@ -264,3 +296,26 @@ func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.Produ
|
|||||||
Image: productDB.Image,
|
Image: productDB.Image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *inprogressOrderRepository) UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error {
|
||||||
|
now := time2.Now()
|
||||||
|
|
||||||
|
result := trx.Model(&models.OrderDB{}).
|
||||||
|
Where("id = ?", orderID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"amount": amount,
|
||||||
|
"tax": tax,
|
||||||
|
"total": total,
|
||||||
|
"updated_at": now,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return errors.Wrap(result.Error, "failed to update order totals")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
logger.ContextLogger(ctx).Warn("no order updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -4,9 +4,10 @@ import (
|
|||||||
"enaklo-pos-be/internal/common/mycontext"
|
"enaklo-pos-be/internal/common/mycontext"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
"enaklo-pos-be/internal/repository/models"
|
"enaklo-pos-be/internal/repository/models"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CashierSessionRepository interface {
|
type CashierSessionRepository interface {
|
||||||
@ -15,6 +16,7 @@ type CashierSessionRepository interface {
|
|||||||
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
|
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
|
||||||
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error)
|
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error)
|
||||||
GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error)
|
GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error)
|
||||||
|
GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type cashierSessionRepository struct {
|
type cashierSessionRepository struct {
|
||||||
@ -27,6 +29,7 @@ func NewCashierSessionRepository(db *gorm.DB) CashierSessionRepository {
|
|||||||
|
|
||||||
func (r *cashierSessionRepository) CreateSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) {
|
func (r *cashierSessionRepository) CreateSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) {
|
||||||
dbModel := models.CashierSessionDB{
|
dbModel := models.CashierSessionDB{
|
||||||
|
PartnerID: session.PartnerID,
|
||||||
CashierID: session.CashierID,
|
CashierID: session.CashierID,
|
||||||
OpenedAt: time.Now(),
|
OpenedAt: time.Now(),
|
||||||
OpeningAmount: session.OpeningAmount,
|
OpeningAmount: session.OpeningAmount,
|
||||||
@ -94,6 +97,7 @@ func (r *cashierSessionRepository) GetSessionByID(ctx mycontext.Context, session
|
|||||||
func (r *cashierSessionRepository) toEntity(db *models.CashierSessionDB) *entity.CashierSession {
|
func (r *cashierSessionRepository) toEntity(db *models.CashierSessionDB) *entity.CashierSession {
|
||||||
return &entity.CashierSession{
|
return &entity.CashierSession{
|
||||||
ID: db.ID,
|
ID: db.ID,
|
||||||
|
PartnerID: db.PartnerID,
|
||||||
CashierID: db.CashierID,
|
CashierID: db.CashierID,
|
||||||
OpenedAt: db.OpenedAt,
|
OpenedAt: db.OpenedAt,
|
||||||
ClosedAt: db.ClosedAt,
|
ClosedAt: db.ClosedAt,
|
||||||
@ -135,3 +139,39 @@ func (r *cashierSessionRepository) GetPaymentSummaryBySessionID(ctx mycontext.Co
|
|||||||
|
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *cashierSessionRepository) GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) {
|
||||||
|
var sessionsDB []models.CashierSessionDB
|
||||||
|
var totalCount int64
|
||||||
|
|
||||||
|
// Count total records
|
||||||
|
if err := r.db.Model(&models.CashierSessionDB{}).
|
||||||
|
Where("partner_id = ?", partnerID).
|
||||||
|
Count(&totalCount).Error; err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "failed to count cashier sessions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sessions with pagination
|
||||||
|
query := r.db.Where("partner_id = ?", partnerID).
|
||||||
|
Order("opened_at DESC")
|
||||||
|
|
||||||
|
if limit > 0 {
|
||||||
|
query = query.Limit(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset > 0 {
|
||||||
|
query = query.Offset(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(&sessionsDB).Error; err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "failed to get cashier session history")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to entity
|
||||||
|
sessions := make([]*entity.CashierSession, len(sessionsDB))
|
||||||
|
for i, sessionDB := range sessionsDB {
|
||||||
|
sessions[i] = r.toEntity(&sessionDB)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions, totalCount, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import "time"
|
|||||||
|
|
||||||
type CashierSessionDB struct {
|
type CashierSessionDB struct {
|
||||||
ID int64 `gorm:"primaryKey"`
|
ID int64 `gorm:"primaryKey"`
|
||||||
|
PartnerID int64 `gorm:"not null"`
|
||||||
CashierID int64 `gorm:"not null"`
|
CashierID int64 `gorm:"not null"`
|
||||||
OpenedAt time.Time `gorm:"not null"`
|
OpenedAt time.Time `gorm:"not null"`
|
||||||
ClosedAt *time.Time
|
ClosedAt *time.Time
|
||||||
|
|||||||
@ -39,6 +39,7 @@ type OrderItemDB struct {
|
|||||||
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"`
|
||||||
|
Status string `gorm:"column:status;default:ACTIVE"`
|
||||||
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"`
|
Product ProductDB `gorm:"foreignKey:ItemID;references:ID"`
|
||||||
|
|||||||
@ -5,10 +5,11 @@ import (
|
|||||||
"enaklo-pos-be/internal/common/mycontext"
|
"enaklo-pos-be/internal/common/mycontext"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
"enaklo-pos-be/internal/repository/models"
|
"enaklo-pos-be/internal/repository/models"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type OrderRepository interface {
|
type OrderRepository interface {
|
||||||
@ -17,32 +18,22 @@ 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)
|
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error)
|
CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error)
|
||||||
|
CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error
|
||||||
|
CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error
|
||||||
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
|
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
|
||||||
GetOrderPaymentMethodBreakdown(
|
GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error)
|
||||||
ctx mycontext.Context,
|
GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error)
|
||||||
partnerID int64,
|
GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error)
|
||||||
req entity.SearchRequest,
|
GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, error)
|
||||||
) ([]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)
|
|
||||||
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
|
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
|
||||||
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error)
|
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error)
|
||||||
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
||||||
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
|
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
|
||||||
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
|
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
|
||||||
|
UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type orderRepository struct {
|
type orderRepository struct {
|
||||||
@ -129,11 +120,6 @@ func (r *orderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Ord
|
|||||||
|
|
||||||
order := r.toDomainOrderModel(&orderDB)
|
order := r.toDomainOrderModel(&orderDB)
|
||||||
|
|
||||||
for _, itemDB := range orderDB.OrderItems {
|
|
||||||
item := r.toDomainOrderItemModel(&itemDB)
|
|
||||||
order.OrderItems = append(order.OrderItems, *item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,15 +267,16 @@ func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order {
|
func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order {
|
||||||
|
|
||||||
orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems))
|
orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems))
|
||||||
for _, itemDB := range dbModel.OrderItems {
|
for _, itemDB := range dbModel.OrderItems {
|
||||||
orderItems = append(orderItems, entity.OrderItem{
|
orderItems = append(orderItems, entity.OrderItem{
|
||||||
|
ID: itemDB.ID,
|
||||||
ItemID: itemDB.ItemID,
|
ItemID: itemDB.ItemID,
|
||||||
ItemType: itemDB.ItemType,
|
ItemType: itemDB.ItemType,
|
||||||
ItemName: itemDB.ItemName,
|
ItemName: itemDB.ItemName,
|
||||||
Price: itemDB.Price,
|
Price: itemDB.Price,
|
||||||
Quantity: itemDB.Quantity,
|
Quantity: itemDB.Quantity,
|
||||||
|
Status: itemDB.Status,
|
||||||
CreatedBy: itemDB.CreatedBy,
|
CreatedBy: itemDB.CreatedBy,
|
||||||
CreatedAt: itemDB.CreatedAt,
|
CreatedAt: itemDB.CreatedAt,
|
||||||
Notes: itemDB.Notes,
|
Notes: itemDB.Notes,
|
||||||
@ -327,6 +314,7 @@ func (r *orderRepository) toOrderItemDBModel(item *entity.OrderItem) models.Orde
|
|||||||
ItemName: item.ItemName,
|
ItemName: item.ItemName,
|
||||||
Price: item.Price,
|
Price: item.Price,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
|
Status: item.Status,
|
||||||
CreatedBy: item.CreatedBy,
|
CreatedBy: item.CreatedBy,
|
||||||
CreatedAt: item.CreatedAt,
|
CreatedAt: item.CreatedAt,
|
||||||
Notes: item.Notes,
|
Notes: item.Notes,
|
||||||
@ -341,9 +329,11 @@ func (r *orderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *e
|
|||||||
ItemType: dbModel.ItemType,
|
ItemType: dbModel.ItemType,
|
||||||
Price: dbModel.Price,
|
Price: dbModel.Price,
|
||||||
Quantity: dbModel.Quantity,
|
Quantity: dbModel.Quantity,
|
||||||
|
Status: dbModel.Status,
|
||||||
CreatedBy: dbModel.CreatedBy,
|
CreatedBy: dbModel.CreatedBy,
|
||||||
CreatedAt: dbModel.CreatedAt,
|
CreatedAt: dbModel.CreatedAt,
|
||||||
ItemName: dbModel.ItemName,
|
ItemName: dbModel.ItemName,
|
||||||
|
Notes: dbModel.Notes,
|
||||||
Product: &entity.Product{
|
Product: &entity.Product{
|
||||||
ID: dbModel.ItemID,
|
ID: dbModel.ItemID,
|
||||||
Name: dbModel.ItemName,
|
Name: dbModel.ItemName,
|
||||||
@ -406,65 +396,70 @@ 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) {
|
func (r *orderRepository) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error) {
|
||||||
var ordersDB []models.OrderDB
|
queryBuilder := NewQueryBuilder[models.OrderDB](r.db)
|
||||||
var totalCount int64
|
filters := []Filter{
|
||||||
|
Equal("partner_id", partnerID),
|
||||||
|
}
|
||||||
|
|
||||||
// Build the base query
|
|
||||||
baseQuery := r.db.Model(&models.OrderDB{}).Where("partner_id = ?", partnerID)
|
|
||||||
|
|
||||||
// Apply filters to the base query
|
|
||||||
if req.Status != "" {
|
if req.Status != "" {
|
||||||
baseQuery = baseQuery.Where("status = ?", req.Status)
|
filters = append(filters, Equal("status", req.Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !req.Start.IsZero() {
|
if !req.Start.IsZero() {
|
||||||
baseQuery = baseQuery.Where("created_at >= ?", req.Start)
|
filters = append(filters, GreaterEqual("created_at", req.Start))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !req.End.IsZero() {
|
if !req.End.IsZero() {
|
||||||
baseQuery = baseQuery.Where("created_at <= ?", req.End)
|
filters = append(filters, LessEqual("created_at", req.End))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get total count with the current filters before pagination
|
options := QueryOptions{
|
||||||
if err := baseQuery.Count(&totalCount).Error; err != nil {
|
Filters: filters,
|
||||||
return nil, 0, errors.Wrap(err, "failed to count total orders")
|
Limit: req.Limit,
|
||||||
|
Offset: req.Offset,
|
||||||
|
OrderBy: []string{"created_at DESC"},
|
||||||
|
Preloads: []string{"OrderItems"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone the query for fetching the actual data with pagination
|
baseQuery := queryBuilder.BuildQuery(options)
|
||||||
query := baseQuery.Session(&gorm.Session{})
|
totalCount, err := queryBuilder.Count(baseQuery)
|
||||||
|
if err != nil {
|
||||||
// Add ordering and pagination
|
return nil, 0, err
|
||||||
query = query.Order("created_at DESC")
|
|
||||||
|
|
||||||
if req.Limit > 0 {
|
|
||||||
query = query.Limit(req.Limit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Offset > 0 {
|
query := queryBuilder.ExecuteQuery(baseQuery, options)
|
||||||
query = query.Offset(req.Offset)
|
ordersDB, err := queryBuilder.Find(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the query with preloading
|
orders := r.convertOrdersToEntity(ordersDB)
|
||||||
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
|
return orders, totalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *orderRepository) convertOrdersToEntity(ordersDB []models.OrderDB) []*entity.Order {
|
||||||
orders := make([]*entity.Order, 0, len(ordersDB))
|
orders := make([]*entity.Order, 0, len(ordersDB))
|
||||||
|
|
||||||
for _, orderDB := range ordersDB {
|
for _, orderDB := range ordersDB {
|
||||||
order := r.toDomainOrderModel(&orderDB)
|
order := r.toDomainOrderModel(&orderDB)
|
||||||
order.OrderItems = make([]entity.OrderItem, 0, len(orderDB.OrderItems))
|
order.OrderItems = r.convertOrderItemsToEntity(orderDB.OrderItems)
|
||||||
|
|
||||||
for _, itemDB := range orderDB.OrderItems {
|
|
||||||
item := r.toDomainOrderItemModel(&itemDB)
|
|
||||||
order.OrderItems = append(order.OrderItems, *item)
|
|
||||||
}
|
|
||||||
|
|
||||||
orders = append(orders, order)
|
orders = append(orders, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
return orders, totalCount, nil
|
return orders
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *orderRepository) convertOrderItemsToEntity(itemsDB []models.OrderItemDB) []entity.OrderItem {
|
||||||
|
items := make([]entity.OrderItem, 0, len(itemsDB))
|
||||||
|
|
||||||
|
for _, itemDB := range itemsDB {
|
||||||
|
item := r.toDomainOrderItemModel(&itemDB)
|
||||||
|
items = append(items, *item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) {
|
func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) {
|
||||||
@ -521,126 +516,108 @@ func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID
|
|||||||
return orders, totalCount, nil
|
return orders, totalCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *orderRepository) CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) {
|
func (r *orderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
|
||||||
isUpdate := order.ID != 0
|
orderDB := r.toOrderDBModel(order)
|
||||||
|
|
||||||
tx := r.db.Begin()
|
// Use provided transaction or create new one
|
||||||
if tx.Error != nil {
|
var dbTx *gorm.DB
|
||||||
return nil, errors.Wrap(tx.Error, "failed to begin transaction")
|
if tx != nil {
|
||||||
|
dbTx = tx
|
||||||
|
} else {
|
||||||
|
dbTx = r.db.Begin()
|
||||||
|
if dbTx.Error != nil {
|
||||||
|
return nil, errors.Wrap(dbTx.Error, "failed to begin transaction")
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
tx.Rollback()
|
dbTx.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 {
|
if order.InProgressOrderID != 0 {
|
||||||
tx.Rollback()
|
// Update existing order
|
||||||
return nil, errors.Wrap(err, "failed to update order")
|
orderDB.ID = order.InProgressOrderID
|
||||||
|
if err := dbTx.Omit("customer_id", "partner_id", "customer_name", "created_by").Save(&orderDB).Error; err != nil {
|
||||||
|
if tx == nil {
|
||||||
|
dbTx.Rollback()
|
||||||
}
|
}
|
||||||
|
return nil, errors.Wrap(err, "failed to update in-progress 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")
|
|
||||||
}
|
}
|
||||||
|
order.ID = order.InProgressOrderID
|
||||||
} else {
|
} else {
|
||||||
if err := tx.Create(&orderDB).Error; err != nil {
|
// Create new order
|
||||||
tx.Rollback()
|
if err := dbTx.Create(&orderDB).Error; err != nil {
|
||||||
|
if tx == nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
}
|
||||||
return nil, errors.Wrap(err, "failed to insert order")
|
return nil, errors.Wrap(err, "failed to insert order")
|
||||||
}
|
}
|
||||||
|
|
||||||
order.ID = orderDB.ID
|
order.ID = orderDB.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemIDs []int64
|
// Only commit if we created the transaction
|
||||||
for i := range order.OrderItems {
|
if tx == nil {
|
||||||
itemIDs = append(itemIDs, order.OrderItems[i].ItemID)
|
if err := dbTx.Commit().Error; err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
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 nil, errors.Wrap(err, "failed to commit transaction")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the order with the ID set, but without items (items will be added separately)
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *orderRepository) toInProgressOrderDBModel(order *entity.Order) models.OrderDB {
|
func (r *orderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
|
||||||
now := time.Now()
|
// Use provided transaction or create new one
|
||||||
|
var dbTx *gorm.DB
|
||||||
return models.OrderDB{
|
if tx != nil {
|
||||||
ID: order.ID,
|
dbTx = tx
|
||||||
PartnerID: order.PartnerID,
|
} else {
|
||||||
CustomerID: order.CustomerID,
|
dbTx = r.db.Begin()
|
||||||
CustomerName: order.CustomerName,
|
if dbTx.Error != nil {
|
||||||
PaymentType: order.PaymentType,
|
return errors.Wrap(dbTx.Error, "failed to begin transaction")
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
itemDB := r.toOrderItemDBModel(&item)
|
||||||
|
itemDB.OrderID = orderID
|
||||||
|
|
||||||
|
if err := dbTx.Create(&itemDB).Error; err != nil {
|
||||||
|
if tx == nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
}
|
||||||
|
return errors.Wrap(err, "failed to insert order item")
|
||||||
|
}
|
||||||
|
|
||||||
|
item.ID = itemDB.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only commit if we created the transaction
|
||||||
|
if tx == nil {
|
||||||
|
if err := dbTx.Commit().Error; err != nil {
|
||||||
|
return errors.Wrap(err, "failed to commit transaction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *orderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product {
|
func (r *orderRepository) CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error {
|
||||||
if productDB == nil {
|
itemDB := r.toOrderItemDBModel(item)
|
||||||
return nil
|
itemDB.OrderID = orderID
|
||||||
|
|
||||||
|
if err := r.db.Create(&itemDB).Error; err != nil {
|
||||||
|
return errors.Wrap(err, "failed to insert order item")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &entity.Product{
|
item.ID = itemDB.ID
|
||||||
ID: productDB.ID,
|
return nil
|
||||||
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) {
|
func (r *orderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) {
|
||||||
@ -954,11 +931,6 @@ func (r *orderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64,
|
|||||||
|
|
||||||
order := r.toDomainOrderModel(&orderDB)
|
order := r.toDomainOrderModel(&orderDB)
|
||||||
|
|
||||||
for _, itemDB := range orderDB.OrderItems {
|
|
||||||
item := r.toDomainOrderItemModel(&itemDB)
|
|
||||||
order.OrderItems = append(order.OrderItems, *item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1025,3 +997,43 @@ func (r *orderRepository) UpdateOrderTotals(ctx mycontext.Context, orderID int64
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *orderRepository) UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
result := trx.Model(&models.OrderDB{}).
|
||||||
|
Where("id = ?", orderID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"amount": amount,
|
||||||
|
"tax": tax,
|
||||||
|
"total": total,
|
||||||
|
"updated_at": now,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return errors.Wrap(result.Error, "failed to update order totals")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
logger.ContextLogger(ctx).Warn("no order updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
193
internal/repository/query_builder.go
Normal file
193
internal/repository/query_builder.go
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueryBuilder[T any] struct {
|
||||||
|
db *gorm.DB
|
||||||
|
model T
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueryBuilder[T any](db *gorm.DB) *QueryBuilder[T] {
|
||||||
|
var model T
|
||||||
|
return &QueryBuilder[T]{
|
||||||
|
db: db,
|
||||||
|
model: model,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
Field string
|
||||||
|
Operator string // "=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "NOT IN", "IS NULL", "IS NOT NULL"
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryOptions struct {
|
||||||
|
Filters []Filter
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
OrderBy []string
|
||||||
|
Preloads []string
|
||||||
|
GroupBy []string
|
||||||
|
Having []Filter
|
||||||
|
Distinct []string
|
||||||
|
CountOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *QueryBuilder[T]) BuildQuery(options QueryOptions) *gorm.DB {
|
||||||
|
query := qb.db.Model(&qb.model)
|
||||||
|
|
||||||
|
for _, filter := range options.Filters {
|
||||||
|
query = qb.applyFilter(query, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(options.Distinct) > 0 {
|
||||||
|
for _, distinct := range options.Distinct {
|
||||||
|
query = query.Distinct(distinct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(options.GroupBy) > 0 {
|
||||||
|
for _, groupBy := range options.GroupBy {
|
||||||
|
query = query.Group(groupBy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, having := range options.Having {
|
||||||
|
query = qb.applyFilter(query, having)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *QueryBuilder[T]) applyFilter(query *gorm.DB, filter Filter) *gorm.DB {
|
||||||
|
switch filter.Operator {
|
||||||
|
case "=", "":
|
||||||
|
return query.Where(filter.Field+" = ?", filter.Value)
|
||||||
|
case "!=":
|
||||||
|
return query.Where(filter.Field+" != ?", filter.Value)
|
||||||
|
case ">":
|
||||||
|
return query.Where(filter.Field+" > ?", filter.Value)
|
||||||
|
case "<":
|
||||||
|
return query.Where(filter.Field+" < ?", filter.Value)
|
||||||
|
case ">=":
|
||||||
|
return query.Where(filter.Field+" >= ?", filter.Value)
|
||||||
|
case "<=":
|
||||||
|
return query.Where(filter.Field+" <= ?", filter.Value)
|
||||||
|
case "LIKE":
|
||||||
|
return query.Where(filter.Field+" LIKE ?", filter.Value)
|
||||||
|
case "IN":
|
||||||
|
return query.Where(filter.Field+" IN ?", filter.Value)
|
||||||
|
case "NOT IN":
|
||||||
|
return query.Where(filter.Field+" NOT IN ?", filter.Value)
|
||||||
|
case "IS NULL":
|
||||||
|
return query.Where(filter.Field + " IS NULL")
|
||||||
|
case "IS NOT NULL":
|
||||||
|
return query.Where(filter.Field + " IS NOT NULL")
|
||||||
|
case "BETWEEN":
|
||||||
|
if values, ok := filter.Value.([]interface{}); ok && len(values) == 2 {
|
||||||
|
return query.Where(filter.Field+" BETWEEN ? AND ?", values[0], values[1])
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
default:
|
||||||
|
return query.Where(filter.Field+" = ?", filter.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *QueryBuilder[T]) ExecuteQuery(baseQuery *gorm.DB, options QueryOptions) *gorm.DB {
|
||||||
|
query := baseQuery.Session(&gorm.Session{})
|
||||||
|
|
||||||
|
if len(options.OrderBy) > 0 {
|
||||||
|
for _, orderBy := range options.OrderBy {
|
||||||
|
query = query.Order(orderBy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, preload := range options.Preloads {
|
||||||
|
query = query.Preload(preload)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Limit > 0 {
|
||||||
|
query = query.Limit(options.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Offset > 0 {
|
||||||
|
query = query.Offset(options.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *QueryBuilder[T]) Count(baseQuery *gorm.DB) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
if err := baseQuery.Count(&count).Error; err != nil {
|
||||||
|
return 0, errors.Wrap(err, "failed to count records")
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *QueryBuilder[T]) Find(query *gorm.DB) ([]T, error) {
|
||||||
|
var results []T
|
||||||
|
if err := query.Find(&results).Error; err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to find records")
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *QueryBuilder[T]) First(query *gorm.DB) (*T, error) {
|
||||||
|
var result T
|
||||||
|
if err := query.First(&result).Error; err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to find record")
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Equal(field string, value interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: "=", Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotEqual(field string, value interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: "!=", Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GreaterThan(field string, value interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: ">", Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LessThan(field string, value interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: "<", Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GreaterEqual(field string, value interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: ">=", Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LessEqual(field string, value interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: "<=", Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Like(field string, value string) Filter {
|
||||||
|
return Filter{Field: field, Operator: "LIKE", Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func In(field string, values interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: "IN", Value: values}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotIn(field string, values interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: "NOT IN", Value: values}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNull(field string) Filter {
|
||||||
|
return Filter{Field: field, Operator: "IS NULL"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNotNull(field string) Filter {
|
||||||
|
return Filter{Field: field, Operator: "IS NOT NULL"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Between(field string, start, end interface{}) Filter {
|
||||||
|
return Filter{Field: field, Operator: "BETWEEN", Value: []interface{}{start, end}}
|
||||||
|
}
|
||||||
@ -16,7 +16,6 @@ import (
|
|||||||
"enaklo-pos-be/internal/repository/products"
|
"enaklo-pos-be/internal/repository/products"
|
||||||
"enaklo-pos-be/internal/repository/sites"
|
"enaklo-pos-be/internal/repository/sites"
|
||||||
transactions "enaklo-pos-be/internal/repository/transaction"
|
transactions "enaklo-pos-be/internal/repository/transaction"
|
||||||
"enaklo-pos-be/internal/repository/trx"
|
|
||||||
"enaklo-pos-be/internal/repository/users"
|
"enaklo-pos-be/internal/repository/users"
|
||||||
repository "enaklo-pos-be/internal/repository/wallet"
|
repository "enaklo-pos-be/internal/repository/wallet"
|
||||||
|
|
||||||
@ -38,7 +37,7 @@ type RepoManagerImpl struct {
|
|||||||
OSS OSSRepository
|
OSS OSSRepository
|
||||||
Partner PartnerRepository
|
Partner PartnerRepository
|
||||||
Site SiteRepository
|
Site SiteRepository
|
||||||
Trx TransactionManager
|
Trx Trx
|
||||||
Wallet WalletRepository
|
Wallet WalletRepository
|
||||||
Midtrans Midtrans
|
Midtrans Midtrans
|
||||||
Payment Payment
|
Payment Payment
|
||||||
@ -70,7 +69,7 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
|
|||||||
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
|
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
|
||||||
Partner: partners.NewPartnerRepository(db),
|
Partner: partners.NewPartnerRepository(db),
|
||||||
Site: sites.NewSiteRepository(db),
|
Site: sites.NewSiteRepository(db),
|
||||||
Trx: trx.NewGormTransactionManager(db),
|
Trx: NewTransactionManager(db),
|
||||||
Wallet: repository.NewWalletRepository(db),
|
Wallet: repository.NewWalletRepository(db),
|
||||||
Midtrans: mdtrns.New(&cfg.Midtrans),
|
Midtrans: mdtrns.New(&cfg.Midtrans),
|
||||||
Payment: payment.NewPaymentRepository(db),
|
Payment: payment.NewPaymentRepository(db),
|
||||||
@ -188,12 +187,6 @@ type SiteRepository interface {
|
|||||||
SearchSites(ctx context.Context, search *entity.DiscoverySearch) ([]entity.SiteProductInfo, int64, error)
|
SearchSites(ctx context.Context, search *entity.DiscoverySearch) ([]entity.SiteProductInfo, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionManager interface {
|
|
||||||
Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error)
|
|
||||||
Commit(session *gorm.DB) *gorm.DB
|
|
||||||
Rollback(session *gorm.DB) *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
type WalletRepository interface {
|
type WalletRepository interface {
|
||||||
Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
||||||
Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
||||||
@ -244,3 +237,9 @@ type PaymentGateway interface {
|
|||||||
CreateQRISPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error)
|
CreateQRISPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error)
|
||||||
CreatePaymentVA(request entity.PaymentRequest) (*entity.PaymentResponse, error)
|
CreatePaymentVA(request entity.PaymentRequest) (*entity.PaymentResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Trx interface {
|
||||||
|
Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error)
|
||||||
|
Commit(session *gorm.DB) *gorm.DB
|
||||||
|
Rollback(session *gorm.DB) *gorm.DB
|
||||||
|
}
|
||||||
|
|||||||
32
internal/repository/transaction.go
Normal file
32
internal/repository/transaction.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransactionManager struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransactionManager(db *gorm.DB) *TransactionManager {
|
||||||
|
return &TransactionManager{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) {
|
||||||
|
tx := tm.db.Begin(opts...)
|
||||||
|
if tx.Error != nil {
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TransactionManager) Commit(session *gorm.DB) *gorm.DB {
|
||||||
|
return session.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TransactionManager) Rollback(session *gorm.DB) *gorm.DB {
|
||||||
|
return session.Rollback()
|
||||||
|
}
|
||||||
@ -10,9 +10,10 @@ import (
|
|||||||
site "enaklo-pos-be/internal/handlers/http/sites"
|
site "enaklo-pos-be/internal/handlers/http/sites"
|
||||||
"enaklo-pos-be/internal/handlers/http/transaction"
|
"enaklo-pos-be/internal/handlers/http/transaction"
|
||||||
"enaklo-pos-be/internal/handlers/http/user"
|
"enaklo-pos-be/internal/handlers/http/user"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"enaklo-pos-be/internal/middlewares"
|
"enaklo-pos-be/internal/middlewares"
|
||||||
|
|
||||||
|
|||||||
@ -20,13 +20,13 @@ type AuthServiceImpl struct {
|
|||||||
user repository.User
|
user repository.User
|
||||||
emailSvc repository.EmailService
|
emailSvc repository.EmailService
|
||||||
emailCfg config.Email
|
emailCfg config.Email
|
||||||
trxRepo repository.TransactionManager
|
trxRepo repository.Trx
|
||||||
license repository.License
|
license repository.License
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(authRepo repository.Auth,
|
func New(authRepo repository.Auth,
|
||||||
crypto repository.Crypto, user repository.User, emailSvc repository.EmailService,
|
crypto repository.Crypto, user repository.User, emailSvc repository.EmailService,
|
||||||
emailCfg config.Email, trxRepo repository.TransactionManager,
|
emailCfg config.Email, trxRepo repository.Trx,
|
||||||
license repository.License,
|
license repository.License,
|
||||||
) *AuthServiceImpl {
|
) *AuthServiceImpl {
|
||||||
return &AuthServiceImpl{
|
return &AuthServiceImpl{
|
||||||
|
|||||||
@ -16,14 +16,14 @@ type Config interface {
|
|||||||
|
|
||||||
type BalanceService struct {
|
type BalanceService struct {
|
||||||
repo repository.WalletRepository
|
repo repository.WalletRepository
|
||||||
trx repository.TransactionManager
|
trx repository.Trx
|
||||||
crypt repository.Crypto
|
crypt repository.Crypto
|
||||||
transaction repository.TransactionRepository
|
transaction repository.TransactionRepository
|
||||||
cfg Config
|
cfg Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBalanceService(repo repository.WalletRepository,
|
func NewBalanceService(repo repository.WalletRepository,
|
||||||
trx repository.TransactionManager,
|
trx repository.Trx,
|
||||||
crypt repository.Crypto, cfg Config,
|
crypt repository.Crypto, cfg Config,
|
||||||
transaction repository.TransactionRepository) *BalanceService {
|
transaction repository.TransactionRepository) *BalanceService {
|
||||||
return &BalanceService{
|
return &BalanceService{
|
||||||
|
|||||||
@ -12,14 +12,14 @@ import (
|
|||||||
|
|
||||||
type PartnerService struct {
|
type PartnerService struct {
|
||||||
repo repository.PartnerRepository
|
repo repository.PartnerRepository
|
||||||
trx repository.TransactionManager
|
trx repository.Trx
|
||||||
userSvc *users.UserService
|
userSvc *users.UserService
|
||||||
walletRepo repository.WalletRepository
|
walletRepo repository.WalletRepository
|
||||||
userRepo repository.User
|
userRepo repository.User
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPartnerService(repo repository.PartnerRepository,
|
func NewPartnerService(repo repository.PartnerRepository,
|
||||||
userSvc *users.UserService, repoManager repository.TransactionManager,
|
userSvc *users.UserService, repoManager repository.Trx,
|
||||||
walletRepo repository.WalletRepository,
|
walletRepo repository.WalletRepository,
|
||||||
userRepo repository.User,
|
userRepo repository.User,
|
||||||
) *PartnerService {
|
) *PartnerService {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
|
|||||||
productSvcV2, custSvcV2, repo.TransactionRepo,
|
productSvcV2, custSvcV2, repo.TransactionRepo,
|
||||||
repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings,
|
repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings,
|
||||||
repo.UndianRepository, cashierSvc)
|
repo.UndianRepository, cashierSvc)
|
||||||
inprogressOrder := inprogress_order.NewInProgressOrderService(repo.OrderRepo, orderService, productSvcV2)
|
inprogressOrder := inprogress_order.NewInProgressOrderService(repo.OrderRepo, orderService, productSvcV2, repo.Trx)
|
||||||
categorySvc := category.New(repo.CategoryRepository)
|
categorySvc := category.New(repo.CategoryRepository)
|
||||||
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),
|
||||||
|
|||||||
@ -13,12 +13,12 @@ import (
|
|||||||
type TransactionService struct {
|
type TransactionService struct {
|
||||||
repo repository.TransactionRepository
|
repo repository.TransactionRepository
|
||||||
wallet repository.WalletRepository
|
wallet repository.WalletRepository
|
||||||
trx repository.TransactionManager
|
trx repository.Trx
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(repo repository.TransactionRepository,
|
func New(repo repository.TransactionRepository,
|
||||||
wallet repository.WalletRepository,
|
wallet repository.WalletRepository,
|
||||||
trx repository.TransactionManager,
|
trx repository.Trx,
|
||||||
) *TransactionService {
|
) *TransactionService {
|
||||||
return &TransactionService{
|
return &TransactionService{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -13,6 +14,7 @@ type Service interface {
|
|||||||
CloseSession(ctx mycontext.Context, sessionID int64, closingAmount float64) (*entity.CashierSessionReport, error)
|
CloseSession(ctx mycontext.Context, sessionID int64, closingAmount float64) (*entity.CashierSessionReport, error)
|
||||||
GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
|
GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
|
||||||
GetSessionReport(ctx mycontext.Context, sessionID int64) (*entity.CashierSessionReport, error)
|
GetSessionReport(ctx mycontext.Context, sessionID int64) (*entity.CashierSessionReport, error)
|
||||||
|
GetSessionHistory(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
@ -21,6 +23,7 @@ type Repository interface {
|
|||||||
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
|
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
|
||||||
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error)
|
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error)
|
||||||
GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error)
|
GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error)
|
||||||
|
GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type cashierSessionSvc struct {
|
type cashierSessionSvc struct {
|
||||||
@ -97,3 +100,11 @@ func (s *cashierSessionSvc) GetSessionReport(ctx mycontext.Context, sessionID in
|
|||||||
Payments: report,
|
Payments: report,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *cashierSessionSvc) GetSessionHistory(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) {
|
||||||
|
sessions, total, err := s.repo.GetSessionHistoryByPartnerID(ctx, partnerID, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "failed to get session history")
|
||||||
|
}
|
||||||
|
return sessions, total, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
package inprogress_order
|
package inprogress_order
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
"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"
|
order2 "enaklo-pos-be/internal/constants/order"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
"enaklo-pos-be/internal/services/v2/order"
|
"enaklo-pos-be/internal/services/v2/order"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -19,56 +25,185 @@ type InProgressOrderService interface {
|
|||||||
|
|
||||||
type OrderRepository interface {
|
type OrderRepository interface {
|
||||||
FindByID(ctx mycontext.Context, id int64) (*entity.Order, error)
|
FindByID(ctx mycontext.Context, id int64) (*entity.Order, error)
|
||||||
CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error)
|
CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error)
|
||||||
|
CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error
|
||||||
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
|
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
|
||||||
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
|
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
|
||||||
|
UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderCalculator interface {
|
type OrderCalculator interface {
|
||||||
CalculateOrderTotals(
|
CalculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, partnerID int64) (*entity.OrderCalculation, error)
|
||||||
ctx mycontext.Context,
|
|
||||||
items []entity.OrderItemRequest,
|
|
||||||
productDetails *entity.ProductDetails,
|
|
||||||
source string,
|
|
||||||
partnerID int64,
|
|
||||||
) (*entity.OrderCalculation, error)
|
|
||||||
ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error)
|
ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransactionManager interface {
|
||||||
|
Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error)
|
||||||
|
Commit(session *gorm.DB) *gorm.DB
|
||||||
|
Rollback(session *gorm.DB) *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
type inProgressOrderSvc struct {
|
type inProgressOrderSvc struct {
|
||||||
repo OrderRepository
|
repo OrderRepository
|
||||||
orderCalculator OrderCalculator
|
orderCalculator OrderCalculator
|
||||||
product order.ProductService
|
product order.ProductService
|
||||||
|
trx TransactionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInProgressOrderService(repo OrderRepository, calculator OrderCalculator, product order.ProductService) InProgressOrderService {
|
func NewInProgressOrderService(repo OrderRepository,
|
||||||
|
calculator OrderCalculator, product order.ProductService, trx TransactionManager) InProgressOrderService {
|
||||||
return &inProgressOrderSvc{
|
return &inProgressOrderSvc{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
orderCalculator: calculator,
|
orderCalculator: calculator,
|
||||||
product: product,
|
product: product,
|
||||||
|
trx: trx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) {
|
func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) {
|
||||||
productIDs, filteredItems, err := s.orderCalculator.ValidateOrderItems(ctx, req.OrderItems)
|
orderItems, err := s.prepareOrderItems(ctx, req.OrderItems, req.PartnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.OrderItems = filteredItems
|
|
||||||
|
|
||||||
productDetails, err := s.product.GetProductDetails(ctx, productIDs, req.PartnerID)
|
orderCalculation, err := s.calculateOrderTotals(ctx, req.OrderItems, req.Source, req.PartnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.trx.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to begin transaction")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
s.trx.Rollback(tx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
orderToSave := s.createOrderEntity(req, nil, orderCalculation) // Save order without items first
|
||||||
|
createdOrder, err := s.repo.CreateOrder(ctx, orderToSave, tx)
|
||||||
|
if err != nil {
|
||||||
|
s.trx.Rollback(tx)
|
||||||
|
if logger.ContextLogger(ctx) != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to create in-progress order", zap.Error(err), zap.Int64("partnerID", orderToSave.PartnerID))
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "failed to create in-progress order")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.repo.CreateOrderItems(ctx, createdOrder.ID, orderItems, tx)
|
||||||
|
if err != nil {
|
||||||
|
s.trx.Rollback(tx)
|
||||||
|
if logger.ContextLogger(ctx) != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to create order items", zap.Error(err), zap.Int64("orderID", createdOrder.ID))
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "failed to create order items")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.trx.Commit(tx).Error; err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to commit transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
fullOrder, err := s.repo.FindByID(ctx, createdOrder.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to fetch created order")
|
||||||
|
}
|
||||||
|
return fullOrder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inProgressOrderSvc) AddItems(ctx mycontext.Context, orderID int64, newItems []entity.OrderItemRequest) (*entity.Order, error) {
|
||||||
|
existingOrder, err := s.repo.FindByID(ctx, orderID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to fetch order %d", orderID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingOrder.Status != order2.Pending.String() {
|
||||||
|
return nil, errors.Errorf("cannot add items to order with status %s", existingOrder.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
newOrderItems, err := s.prepareOrderItems(ctx, newItems, existingOrder.PartnerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.trx.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to begin transaction")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
s.trx.Rollback(tx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = s.repo.CreateOrderItems(ctx, existingOrder.ID, newOrderItems, tx)
|
||||||
|
if err != nil {
|
||||||
|
s.trx.Rollback(tx)
|
||||||
|
if logger.ContextLogger(ctx) != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to add order items",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Int64("orderID", existingOrder.ID))
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "failed to add order items")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedOrder, err := s.repo.FindByID(ctx, existingOrder.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.trx.Rollback(tx)
|
||||||
|
return nil, errors.Wrap(err, "failed to fetch updated order")
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedItemRequests := s.convertToOrderItemRequests(updatedOrder.OrderItems)
|
||||||
|
orderCalculation, err := s.calculateOrderTotals(ctx, combinedItemRequests, updatedOrder.Source, updatedOrder.PartnerID)
|
||||||
|
if err != nil {
|
||||||
|
s.trx.Rollback(tx)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedOrder.Total = orderCalculation.Total
|
||||||
|
updatedOrder.Tax = orderCalculation.Tax
|
||||||
|
updatedOrder.Amount = orderCalculation.Subtotal
|
||||||
|
|
||||||
|
err = s.repo.UpdateOrderTotalsWithTx(ctx,
|
||||||
|
tx,
|
||||||
|
updatedOrder.ID,
|
||||||
|
orderCalculation.Subtotal,
|
||||||
|
orderCalculation.Tax,
|
||||||
|
orderCalculation.Total)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.trx.Rollback(tx)
|
||||||
|
if logger.ContextLogger(ctx) != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err), zap.Int64("orderID", updatedOrder.ID))
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "failed to update order totals")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.trx.Commit(tx).Error; err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to commit transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedOrder.OrderItems = newOrderItems
|
||||||
|
|
||||||
|
return updatedOrder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inProgressOrderSvc) prepareOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest, partnerID int64) ([]entity.OrderItem, error) {
|
||||||
|
productIDs, filteredItems, err := s.orderCalculator.ValidateOrderItems(ctx, items)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID)
|
||||||
|
if err != nil {
|
||||||
|
if logger.ContextLogger(ctx) != nil {
|
||||||
logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err))
|
logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err))
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
orderCalculation, err := s.orderCalculator.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source, req.PartnerID)
|
orderItems := make([]entity.OrderItem, len(filteredItems))
|
||||||
if err != nil {
|
for i, item := range filteredItems {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
orderItems := make([]entity.OrderItem, len(req.OrderItems))
|
|
||||||
for i, item := range req.OrderItems {
|
|
||||||
product, exists := productDetails.Products[item.ProductID]
|
product, exists := productDetails.Products[item.ProductID]
|
||||||
productName := ""
|
productName := ""
|
||||||
if exists {
|
if exists {
|
||||||
@ -81,12 +216,29 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques
|
|||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
Price: product.Price,
|
Price: product.Price,
|
||||||
ItemType: product.Type,
|
ItemType: product.Type,
|
||||||
Description: product.Description,
|
|
||||||
Notes: item.Notes,
|
Notes: item.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
order := &entity.Order{
|
return orderItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inProgressOrderSvc) calculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, source string, partnerID int64) (*entity.OrderCalculation, error) {
|
||||||
|
productIDs, _, err := s.orderCalculator.ValidateOrderItems(ctx, items)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.orderCalculator.CalculateOrderTotals(ctx, items, productDetails, source, partnerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inProgressOrderSvc) createOrderEntity(req *entity.OrderRequest, orderItems []entity.OrderItem, calculation *entity.OrderCalculation) *entity.Order {
|
||||||
|
return &entity.Order{
|
||||||
ID: req.ID,
|
ID: req.ID,
|
||||||
PartnerID: req.PartnerID,
|
PartnerID: req.PartnerID,
|
||||||
CustomerID: req.CustomerID,
|
CustomerID: req.CustomerID,
|
||||||
@ -95,22 +247,50 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques
|
|||||||
OrderItems: orderItems,
|
OrderItems: orderItems,
|
||||||
TableNumber: req.TableNumber,
|
TableNumber: req.TableNumber,
|
||||||
OrderType: req.OrderType,
|
OrderType: req.OrderType,
|
||||||
Total: orderCalculation.Total,
|
Total: calculation.Total,
|
||||||
Tax: orderCalculation.Tax,
|
Tax: calculation.Tax,
|
||||||
Amount: orderCalculation.Subtotal,
|
Amount: calculation.Subtotal,
|
||||||
Status: order2.Pending.String(),
|
Status: order2.Pending.String(),
|
||||||
Source: req.Source,
|
Source: req.Source,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createdOrder, err := s.repo.CreateOrUpdate(ctx, order)
|
func (s *inProgressOrderSvc) convertToOrderItemRequests(items []entity.OrderItem) []entity.OrderItemRequest {
|
||||||
if err != nil {
|
requests := make([]entity.OrderItemRequest, len(items))
|
||||||
logger.ContextLogger(ctx).Error("failed to create in-progress order",
|
for i, item := range items {
|
||||||
zap.Error(err),
|
requests[i] = entity.OrderItemRequest{
|
||||||
zap.Int64("partnerID", order.PartnerID))
|
ProductID: item.ItemID,
|
||||||
return nil, errors.Wrap(err, "failed to create in-progress order")
|
Quantity: item.Quantity,
|
||||||
|
Notes: item.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requests
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inProgressOrderSvc) extractNewlyAddedItems(updatedOrder *entity.Order, existingItems []entity.OrderItem) []entity.OrderItem {
|
||||||
|
if len(existingItems) == 0 {
|
||||||
|
return updatedOrder.OrderItems
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdOrder, nil
|
existingItemMap := make(map[string]struct{})
|
||||||
|
for _, item := range existingItems {
|
||||||
|
key := s.createItemKey(item)
|
||||||
|
existingItemMap[key] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
newlyAdded := make([]entity.OrderItem, 0)
|
||||||
|
for _, item := range updatedOrder.OrderItems {
|
||||||
|
key := s.createItemKey(item)
|
||||||
|
if _, exists := existingItemMap[key]; !exists {
|
||||||
|
newlyAdded = append(newlyAdded, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newlyAdded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inProgressOrderSvc) createItemKey(item entity.OrderItem) string {
|
||||||
|
return fmt.Sprintf("%d_%s", item.ItemID, item.Notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) {
|
func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) {
|
||||||
@ -127,79 +307,6 @@ func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partner
|
|||||||
return orders, nil
|
return orders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *inProgressOrderSvc) AddItems(ctx mycontext.Context, orderID int64, newItems []entity.OrderItemRequest) (*entity.Order, error) {
|
|
||||||
existingOrder, err := s.repo.FindByID(ctx, orderID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to fetch order %d", orderID)
|
|
||||||
}
|
|
||||||
|
|
||||||
type itemKey struct {
|
|
||||||
ProductID int64
|
|
||||||
Notes string
|
|
||||||
}
|
|
||||||
|
|
||||||
itemMap := make(map[itemKey]entity.OrderItemRequest)
|
|
||||||
existingKeys := make(map[itemKey]struct{})
|
|
||||||
|
|
||||||
// Collect existing items
|
|
||||||
for _, oi := range existingOrder.OrderItems {
|
|
||||||
key := itemKey{ProductID: oi.ItemID, Notes: oi.Notes}
|
|
||||||
existingKeys[key] = struct{}{}
|
|
||||||
itemMap[key] = entity.OrderItemRequest{
|
|
||||||
ProductID: oi.ItemID,
|
|
||||||
Quantity: oi.Quantity,
|
|
||||||
Notes: oi.Notes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge new items into map
|
|
||||||
for _, ni := range newItems {
|
|
||||||
key := itemKey{ProductID: ni.ProductID, Notes: ni.Notes}
|
|
||||||
if existing, found := itemMap[key]; found {
|
|
||||||
existing.Quantity += ni.Quantity
|
|
||||||
itemMap[key] = existing
|
|
||||||
} else {
|
|
||||||
itemMap[key] = ni
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare merged items
|
|
||||||
mergedItems := make([]entity.OrderItemRequest, 0, len(itemMap))
|
|
||||||
for _, item := range itemMap {
|
|
||||||
mergedItems = append(mergedItems, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save updated order
|
|
||||||
req := &entity.OrderRequest{
|
|
||||||
ID: existingOrder.ID,
|
|
||||||
PartnerID: existingOrder.PartnerID,
|
|
||||||
CustomerID: existingOrder.CustomerID,
|
|
||||||
CustomerName: existingOrder.CustomerName,
|
|
||||||
CreatedBy: existingOrder.CreatedBy,
|
|
||||||
TableNumber: existingOrder.TableNumber,
|
|
||||||
OrderType: existingOrder.OrderType,
|
|
||||||
Source: existingOrder.Source,
|
|
||||||
OrderItems: mergedItems,
|
|
||||||
}
|
|
||||||
|
|
||||||
savedOrder, err := s.Save(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
newlyAdded := make([]entity.OrderItem, 0)
|
|
||||||
for _, item := range savedOrder.OrderItems {
|
|
||||||
key := itemKey{ProductID: item.ItemID, Notes: item.Notes}
|
|
||||||
if _, exists := existingKeys[key]; !exists {
|
|
||||||
newlyAdded = append(newlyAdded, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
savedOrder.OrderItems = newlyAdded
|
|
||||||
|
|
||||||
return savedOrder, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *inProgressOrderSvc) GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) {
|
func (s *inProgressOrderSvc) GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) {
|
||||||
orders, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
orders, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
898
internal/services/v2/inprogress_order/in_progress_order_test.go
Normal file
898
internal/services/v2/inprogress_order/in_progress_order_test.go
Normal file
@ -0,0 +1,898 @@
|
|||||||
|
package inprogress_order
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"enaklo-pos-be/internal/common/mycontext"
|
||||||
|
order2 "enaklo-pos-be/internal/constants/order"
|
||||||
|
"enaklo-pos-be/internal/entity"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock implementations
|
||||||
|
type MockOrderRepository struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockOrderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) {
|
||||||
|
args := m.Called(ctx, id)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*entity.Order), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockOrderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
|
||||||
|
args := m.Called(ctx, order, tx)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*entity.Order), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockOrderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
|
||||||
|
args := m.Called(ctx, orderID, items, tx)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) {
|
||||||
|
args := m.Called(ctx, partnerID, limit, offset, status)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).([]*entity.Order), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockOrderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) {
|
||||||
|
args := m.Called(ctx, id, partnerID)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*entity.Order), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockOrderCalculator struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockOrderCalculator) CalculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, partnerID int64) (*entity.OrderCalculation, error) {
|
||||||
|
args := m.Called(ctx, items, productDetails, source, partnerID)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*entity.OrderCalculation), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockOrderCalculator) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) {
|
||||||
|
args := m.Called(ctx, items)
|
||||||
|
|
||||||
|
// Handle nil values properly
|
||||||
|
var productIDs []int64
|
||||||
|
if args.Get(0) != nil {
|
||||||
|
productIDs = args.Get(0).([]int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
var filteredItems []entity.OrderItemRequest
|
||||||
|
if args.Get(1) != nil {
|
||||||
|
filteredItems = args.Get(1).([]entity.OrderItemRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return productIDs, filteredItems, args.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockProductService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockProductService) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) {
|
||||||
|
args := m.Called(ctx, productIDs, partnerID)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*entity.ProductDetails), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockProductService) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) {
|
||||||
|
args := m.Called(ctx, ids, partnerID)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).([]*entity.Product), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockTransactionManager struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) {
|
||||||
|
args := m.Called(ctx, opts)
|
||||||
|
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTransactionManager) Commit(session *gorm.DB) *gorm.DB {
|
||||||
|
args := m.Called(session)
|
||||||
|
return args.Get(0).(*gorm.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTransactionManager) Rollback(session *gorm.DB) *gorm.DB {
|
||||||
|
args := m.Called(session)
|
||||||
|
return args.Get(0).(*gorm.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInProgressOrderService_Save(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request *entity.OrderRequest
|
||||||
|
setupMocks func(*MockOrderRepository, *MockOrderCalculator, *MockProductService)
|
||||||
|
expectedResult *entity.Order
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful order creation",
|
||||||
|
request: &entity.OrderRequest{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
CustomerID: func() *int64 { id := int64(200); return &id }(),
|
||||||
|
CustomerName: "John Doe",
|
||||||
|
CreatedBy: 300,
|
||||||
|
OrderItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 2, Notes: "Extra spicy"},
|
||||||
|
{ProductID: 2, Quantity: 1, Notes: ""},
|
||||||
|
},
|
||||||
|
TableNumber: "A1",
|
||||||
|
OrderType: "DINE_IN",
|
||||||
|
Source: "POS",
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
// Mock ValidateOrderItems
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
[]int64{1, 2},
|
||||||
|
[]entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 2, Notes: "Extra spicy"},
|
||||||
|
{ProductID: 2, Quantity: 1, Notes: ""},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock GetProductDetails
|
||||||
|
productDetails := &entity.ProductDetails{
|
||||||
|
Products: map[int64]*entity.Product{
|
||||||
|
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
|
||||||
|
2: {ID: 2, Name: "Fries", Price: 5.0, Type: "PRODUCT"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
prod.On("GetProductDetails", mock.Anything, []int64{1, 2}, int64(100)).Return(productDetails, nil)
|
||||||
|
|
||||||
|
// Mock CalculateOrderTotals
|
||||||
|
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "POS", int64(100)).Return(
|
||||||
|
&entity.OrderCalculation{
|
||||||
|
Subtotal: 25.0,
|
||||||
|
Tax: 2.5,
|
||||||
|
Total: 27.5,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock CreateOrder (returns order without items)
|
||||||
|
createdOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
CustomerID: func() *int64 { id := int64(200); return &id }(),
|
||||||
|
CustomerName: "John Doe",
|
||||||
|
CreatedBy: 300,
|
||||||
|
TableNumber: "A1",
|
||||||
|
OrderType: "DINE_IN",
|
||||||
|
Total: 27.5,
|
||||||
|
Tax: 2.5,
|
||||||
|
Amount: 25.0,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
Source: "POS",
|
||||||
|
}
|
||||||
|
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(createdOrder, nil)
|
||||||
|
|
||||||
|
// Mock CreateOrderItems
|
||||||
|
repo.On("CreateOrderItems", mock.Anything, int64(1), mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// Mock FindByID (returns full order with items)
|
||||||
|
expectedOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
CustomerID: func() *int64 { id := int64(200); return &id }(),
|
||||||
|
CustomerName: "John Doe",
|
||||||
|
CreatedBy: 300,
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
|
||||||
|
{ItemID: 2, ItemName: "Fries", Quantity: 1, Price: 5.0, ItemType: "PRODUCT", Notes: ""},
|
||||||
|
},
|
||||||
|
TableNumber: "A1",
|
||||||
|
OrderType: "DINE_IN",
|
||||||
|
Total: 27.5,
|
||||||
|
Tax: 2.5,
|
||||||
|
Amount: 25.0,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
Source: "POS",
|
||||||
|
}
|
||||||
|
repo.On("FindByID", mock.Anything, int64(1)).Return(expectedOrder, nil)
|
||||||
|
},
|
||||||
|
expectedResult: &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
CustomerID: func() *int64 { id := int64(200); return &id }(),
|
||||||
|
CustomerName: "John Doe",
|
||||||
|
CreatedBy: 300,
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
|
||||||
|
{ItemID: 2, ItemName: "Fries", Quantity: 1, Price: 5.0, ItemType: "PRODUCT", Notes: ""},
|
||||||
|
},
|
||||||
|
TableNumber: "A1",
|
||||||
|
OrderType: "DINE_IN",
|
||||||
|
Total: 27.5,
|
||||||
|
Tax: 2.5,
|
||||||
|
Amount: 25.0,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
Source: "POS",
|
||||||
|
},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validation error",
|
||||||
|
request: &entity.OrderRequest{
|
||||||
|
PartnerID: 100,
|
||||||
|
OrderItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 0}, // Invalid quantity
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
nil, nil, errors.New("invalid quantity"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "invalid quantity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "product details error",
|
||||||
|
request: &entity.OrderRequest{
|
||||||
|
PartnerID: 100,
|
||||||
|
OrderItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
[]int64{1},
|
||||||
|
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(nil, errors.New("product not found"))
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "product not found",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "calculation error",
|
||||||
|
request: &entity.OrderRequest{
|
||||||
|
PartnerID: 100,
|
||||||
|
OrderItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
[]int64{1},
|
||||||
|
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
productDetails := &entity.ProductDetails{
|
||||||
|
Products: map[int64]*entity.Product{
|
||||||
|
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
|
||||||
|
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "", int64(100)).Return(
|
||||||
|
nil, errors.New("calculation failed"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "calculation failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repository error",
|
||||||
|
request: &entity.OrderRequest{
|
||||||
|
PartnerID: 100,
|
||||||
|
OrderItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
[]int64{1},
|
||||||
|
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
productDetails := &entity.ProductDetails{
|
||||||
|
Products: map[int64]*entity.Product{
|
||||||
|
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
|
||||||
|
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "", int64(100)).Return(
|
||||||
|
&entity.OrderCalculation{Subtotal: 10.0, Tax: 1.0, Total: 11.0},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(nil, errors.New("database error"))
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "failed to create in-progress order: database error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Setup mocks
|
||||||
|
mockRepo := &MockOrderRepository{}
|
||||||
|
mockCalc := &MockOrderCalculator{}
|
||||||
|
mockProd := &MockProductService{}
|
||||||
|
|
||||||
|
if tt.setupMocks != nil {
|
||||||
|
tt.setupMocks(mockRepo, mockCalc, mockProd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create service
|
||||||
|
service := NewInProgressOrderService(mockRepo, mockCalc, mockProd)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
ctx := mycontext.NewContext(context.Background())
|
||||||
|
result, err := service.Save(ctx, tt.request)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if tt.expectedError != "" {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.expectedError)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, tt.expectedResult.ID, result.ID)
|
||||||
|
assert.Equal(t, tt.expectedResult.PartnerID, result.PartnerID)
|
||||||
|
assert.Equal(t, tt.expectedResult.Status, result.Status)
|
||||||
|
assert.Equal(t, tt.expectedResult.Total, result.Total)
|
||||||
|
assert.Len(t, result.OrderItems, len(tt.expectedResult.OrderItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all mocks were called as expected
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
mockCalc.AssertExpectations(t)
|
||||||
|
mockProd.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInProgressOrderService_AddItems(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
orderID int64
|
||||||
|
newItems []entity.OrderItemRequest
|
||||||
|
setupMocks func(*MockOrderRepository, *MockOrderCalculator, *MockProductService)
|
||||||
|
expectedResult *entity.Order
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful add items to pending order",
|
||||||
|
orderID: 1,
|
||||||
|
newItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 3, Quantity: 1, Notes: "No onions"},
|
||||||
|
{ProductID: 4, Quantity: 2, Notes: ""},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
// Mock existing order
|
||||||
|
existingOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
|
||||||
|
},
|
||||||
|
Source: "POS",
|
||||||
|
}
|
||||||
|
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil).Once()
|
||||||
|
|
||||||
|
// Mock ValidateOrderItems for new items
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
[]int64{3, 4},
|
||||||
|
[]entity.OrderItemRequest{
|
||||||
|
{ProductID: 3, Quantity: 1, Notes: "No onions"},
|
||||||
|
{ProductID: 4, Quantity: 2, Notes: ""},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock GetProductDetails
|
||||||
|
productDetails := &entity.ProductDetails{
|
||||||
|
Products: map[int64]*entity.Product{
|
||||||
|
3: {ID: 3, Name: "Salad", Price: 8.0, Type: "PRODUCT"},
|
||||||
|
4: {ID: 4, Name: "Drink", Price: 3.0, Type: "PRODUCT"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
prod.On("GetProductDetails", mock.Anything, []int64{3, 4}, int64(100)).Return(productDetails, nil)
|
||||||
|
|
||||||
|
// Mock CreateOrderItems
|
||||||
|
repo.On("CreateOrderItems", mock.Anything, int64(1), mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// Mock FindByID (returns updated order with all items)
|
||||||
|
updatedOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
|
||||||
|
{ItemID: 3, ItemName: "Salad", Quantity: 1, Price: 8.0, ItemType: "PRODUCT", Notes: "No onions"},
|
||||||
|
{ItemID: 4, ItemName: "Drink", Quantity: 2, Price: 3.0, ItemType: "PRODUCT", Notes: ""},
|
||||||
|
},
|
||||||
|
Total: 40.7,
|
||||||
|
Tax: 3.7,
|
||||||
|
Amount: 37.0,
|
||||||
|
Source: "POS",
|
||||||
|
}
|
||||||
|
repo.On("FindByID", mock.Anything, int64(1)).Return(updatedOrder, nil).Once()
|
||||||
|
|
||||||
|
// Mock CalculateOrderTotals for combined items
|
||||||
|
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
|
||||||
|
&entity.OrderCalculation{
|
||||||
|
Subtotal: 37.0,
|
||||||
|
Tax: 3.7,
|
||||||
|
Total: 40.7,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock CreateOrder for updating totals
|
||||||
|
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(updatedOrder, nil)
|
||||||
|
},
|
||||||
|
expectedResult: &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 3, ItemName: "Salad", Quantity: 1, Price: 8.0, ItemType: "PRODUCT", Notes: "No onions"},
|
||||||
|
{ItemID: 4, ItemName: "Drink", Quantity: 2, Price: 3.0, ItemType: "PRODUCT", Notes: ""},
|
||||||
|
},
|
||||||
|
Total: 40.7,
|
||||||
|
Tax: 3.7,
|
||||||
|
Amount: 37.0,
|
||||||
|
Source: "POS",
|
||||||
|
},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "order not found",
|
||||||
|
orderID: 999,
|
||||||
|
newItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 1},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
repo.On("FindByID", mock.Anything, int64(999)).Return(nil, errors.New("order not found"))
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "failed to fetch order 999: order not found",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "order not in pending status",
|
||||||
|
orderID: 1,
|
||||||
|
newItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 1},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
existingOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
Status: order2.Paid.String(),
|
||||||
|
}
|
||||||
|
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "cannot add items to order with status PAID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validation error for new items",
|
||||||
|
orderID: 1,
|
||||||
|
newItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 0}, // Invalid quantity
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
existingOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
nil, nil, errors.New("invalid quantity"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "invalid quantity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "product details error",
|
||||||
|
orderID: 1,
|
||||||
|
newItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 1},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
existingOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
[]int64{1},
|
||||||
|
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(nil, errors.New("product not found"))
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "product not found",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "calculation error",
|
||||||
|
orderID: 1,
|
||||||
|
newItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 1},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
existingOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
|
||||||
|
},
|
||||||
|
Source: "POS",
|
||||||
|
}
|
||||||
|
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
[]int64{1},
|
||||||
|
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
productDetails := &entity.ProductDetails{
|
||||||
|
Products: map[int64]*entity.Product{
|
||||||
|
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
|
||||||
|
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
|
||||||
|
nil, errors.New("calculation failed"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "calculation failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repository update error",
|
||||||
|
orderID: 1,
|
||||||
|
newItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 1},
|
||||||
|
},
|
||||||
|
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
|
||||||
|
existingOrder := &entity.Order{
|
||||||
|
ID: 1,
|
||||||
|
PartnerID: 100,
|
||||||
|
Status: order2.Pending.String(),
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
|
||||||
|
},
|
||||||
|
Source: "POS",
|
||||||
|
}
|
||||||
|
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
|
||||||
|
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
|
||||||
|
[]int64{1},
|
||||||
|
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
productDetails := &entity.ProductDetails{
|
||||||
|
Products: map[int64]*entity.Product{
|
||||||
|
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
|
||||||
|
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
|
||||||
|
&entity.OrderCalculation{Subtotal: 30.0, Tax: 3.0, Total: 33.0},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
repo.On("CreateOrderItems", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("database error"))
|
||||||
|
},
|
||||||
|
expectedResult: nil,
|
||||||
|
expectedError: "failed to add order items: database error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Setup mocks
|
||||||
|
mockRepo := &MockOrderRepository{}
|
||||||
|
mockCalc := &MockOrderCalculator{}
|
||||||
|
mockProd := &MockProductService{}
|
||||||
|
|
||||||
|
if tt.setupMocks != nil {
|
||||||
|
tt.setupMocks(mockRepo, mockCalc, mockProd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create service
|
||||||
|
service := NewInProgressOrderService(mockRepo, mockCalc, mockProd)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
ctx := mycontext.NewContext(context.Background())
|
||||||
|
result, err := service.AddItems(ctx, tt.orderID, tt.newItems)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if tt.expectedError != "" {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.expectedError)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, tt.expectedResult.ID, result.ID)
|
||||||
|
assert.Equal(t, tt.expectedResult.PartnerID, result.PartnerID)
|
||||||
|
assert.Equal(t, tt.expectedResult.Status, result.Status)
|
||||||
|
assert.Equal(t, tt.expectedResult.Total, result.Total)
|
||||||
|
// Should only return newly added items
|
||||||
|
assert.Len(t, result.OrderItems, len(tt.expectedResult.OrderItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all mocks were called as expected
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
mockCalc.AssertExpectations(t)
|
||||||
|
mockProd.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInProgressOrderService_HelperMethods(t *testing.T) {
|
||||||
|
service := &inProgressOrderSvc{}
|
||||||
|
|
||||||
|
t.Run("convertToOrderItemRequests", func(t *testing.T) {
|
||||||
|
items := []entity.OrderItem{
|
||||||
|
{ItemID: 1, Quantity: 2, Notes: "Extra spicy"},
|
||||||
|
{ItemID: 2, Quantity: 1, Notes: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := service.convertToOrderItemRequests(items)
|
||||||
|
|
||||||
|
assert.Len(t, result, 2)
|
||||||
|
assert.Equal(t, int64(1), result[0].ProductID)
|
||||||
|
assert.Equal(t, 2, result[0].Quantity)
|
||||||
|
assert.Equal(t, "Extra spicy", result[0].Notes)
|
||||||
|
assert.Equal(t, int64(2), result[1].ProductID)
|
||||||
|
assert.Equal(t, 1, result[1].Quantity)
|
||||||
|
assert.Equal(t, "", result[1].Notes)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("createItemKey", func(t *testing.T) {
|
||||||
|
item := entity.OrderItem{ItemID: 1, Notes: "Extra spicy"}
|
||||||
|
key := service.createItemKey(item)
|
||||||
|
assert.Equal(t, "1_Extra spicy", key)
|
||||||
|
|
||||||
|
item2 := entity.OrderItem{ItemID: 2, Notes: ""}
|
||||||
|
key2 := service.createItemKey(item2)
|
||||||
|
assert.Equal(t, "2_", key2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("extractNewlyAddedItems", func(t *testing.T) {
|
||||||
|
existingItems := []entity.OrderItem{
|
||||||
|
{ItemID: 1, Notes: "Extra spicy"},
|
||||||
|
{ItemID: 2, Notes: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedOrder := &entity.Order{
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, Notes: "Extra spicy"},
|
||||||
|
{ItemID: 2, Notes: ""},
|
||||||
|
{ItemID: 3, Notes: "No onions"},
|
||||||
|
{ItemID: 4, Notes: ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := service.extractNewlyAddedItems(updatedOrder, existingItems)
|
||||||
|
|
||||||
|
assert.Len(t, result, 2)
|
||||||
|
assert.Equal(t, int64(3), result[0].ItemID)
|
||||||
|
assert.Equal(t, "No onions", result[0].Notes)
|
||||||
|
assert.Equal(t, int64(4), result[1].ItemID)
|
||||||
|
assert.Equal(t, "", result[1].Notes)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("extractNewlyAddedItems with no existing items", func(t *testing.T) {
|
||||||
|
updatedOrder := &entity.Order{
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, Notes: "Extra spicy"},
|
||||||
|
{ItemID: 2, Notes: ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := service.extractNewlyAddedItems(updatedOrder, []entity.OrderItem{})
|
||||||
|
|
||||||
|
assert.Len(t, result, 2)
|
||||||
|
assert.Equal(t, int64(1), result[0].ItemID)
|
||||||
|
assert.Equal(t, int64(2), result[1].ItemID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSave_WithTransaction(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockRepo := new(MockOrderRepository)
|
||||||
|
mockCalculator := new(MockOrderCalculator)
|
||||||
|
mockProduct := new(MockProductService)
|
||||||
|
mockTrx := new(MockTransactionManager)
|
||||||
|
|
||||||
|
service := NewInProgressOrderService(mockRepo, mockCalculator, mockProduct, mockTrx)
|
||||||
|
|
||||||
|
ctx := mycontext.NewContext(context.Background())
|
||||||
|
req := &entity.OrderRequest{
|
||||||
|
PartnerID: 1,
|
||||||
|
OrderItems: []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 2},
|
||||||
|
},
|
||||||
|
Source: "pos",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock transaction
|
||||||
|
mockTx := &gorm.DB{}
|
||||||
|
mockTrx.On("Begin", ctx, mock.Anything).Return(mockTx, nil)
|
||||||
|
mockTrx.On("Commit", mockTx).Return(mockTx)
|
||||||
|
mockTrx.On("Rollback", mockTx).Return(mockTx)
|
||||||
|
|
||||||
|
// Mock calculator
|
||||||
|
productIDs := []int64{1}
|
||||||
|
filteredItems := []entity.OrderItemRequest{{ProductID: 1, Quantity: 2}}
|
||||||
|
mockCalculator.On("ValidateOrderItems", ctx, req.OrderItems).Return(productIDs, filteredItems, nil)
|
||||||
|
|
||||||
|
// Mock product service
|
||||||
|
productDetails := &entity.ProductDetails{
|
||||||
|
Products: map[int64]*entity.Product{
|
||||||
|
1: {ID: 1, Name: "Test Product", Price: 10.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockProduct.On("GetProductDetails", ctx, productIDs, req.PartnerID).Return(productDetails, nil)
|
||||||
|
|
||||||
|
// Mock calculation
|
||||||
|
calculation := &entity.OrderCalculation{
|
||||||
|
Subtotal: 20.0,
|
||||||
|
Tax: 2.0,
|
||||||
|
Total: 22.0,
|
||||||
|
}
|
||||||
|
mockCalculator.On("CalculateOrderTotals", ctx, req.OrderItems, productDetails, req.Source, req.PartnerID).Return(calculation, nil)
|
||||||
|
|
||||||
|
// Mock repository calls
|
||||||
|
createdOrder := &entity.Order{ID: 1, PartnerID: 1}
|
||||||
|
mockRepo.On("CreateOrder", ctx, mock.AnythingOfType("*entity.Order"), mockTx).Return(createdOrder, nil)
|
||||||
|
mockRepo.On("CreateOrderItems", ctx, int64(1), mock.AnythingOfType("[]entity.OrderItem"), mockTx).Return(nil)
|
||||||
|
|
||||||
|
fullOrder := &entity.Order{ID: 1, PartnerID: 1, OrderItems: []entity.OrderItem{}}
|
||||||
|
mockRepo.On("FindByID", ctx, int64(1)).Return(fullOrder, nil)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
result, err := service.Save(ctx, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, int64(1), result.ID)
|
||||||
|
|
||||||
|
// Verify all mocks were called
|
||||||
|
mockTrx.AssertExpectations(t)
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
mockCalculator.AssertExpectations(t)
|
||||||
|
mockProduct.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddItems_WithTransaction(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockRepo := new(MockOrderRepository)
|
||||||
|
mockCalculator := new(MockOrderCalculator)
|
||||||
|
mockProduct := new(MockProductService)
|
||||||
|
mockTrx := new(MockTransactionManager)
|
||||||
|
|
||||||
|
service := NewInProgressOrderService(mockRepo, mockCalculator, mockProduct, mockTrx)
|
||||||
|
|
||||||
|
ctx := mycontext.NewContext(context.Background())
|
||||||
|
orderID := int64(1)
|
||||||
|
newItems := []entity.OrderItemRequest{
|
||||||
|
{ProductID: 2, Quantity: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock existing order
|
||||||
|
existingOrder := &entity.Order{
|
||||||
|
ID: orderID,
|
||||||
|
Status: "pending",
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, Quantity: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockRepo.On("FindByID", ctx, orderID).Return(existingOrder, nil)
|
||||||
|
|
||||||
|
// Mock transaction
|
||||||
|
mockTx := &gorm.DB{}
|
||||||
|
mockTrx.On("Begin", ctx, mock.Anything).Return(mockTx, nil)
|
||||||
|
mockTrx.On("Commit", mockTx).Return(mockTx)
|
||||||
|
mockTrx.On("Rollback", mockTx).Return(mockTx)
|
||||||
|
|
||||||
|
// Mock calculator
|
||||||
|
productIDs := []int64{2}
|
||||||
|
filteredItems := []entity.OrderItemRequest{{ProductID: 2, Quantity: 1}}
|
||||||
|
mockCalculator.On("ValidateOrderItems", ctx, newItems).Return(productIDs, filteredItems, nil)
|
||||||
|
|
||||||
|
// Mock product service
|
||||||
|
productDetails := &entity.ProductDetails{
|
||||||
|
Products: map[int64]*entity.Product{
|
||||||
|
2: {ID: 2, Name: "New Product", Price: 15.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockProduct.On("GetProductDetails", ctx, productIDs, existingOrder.PartnerID).Return(productDetails, nil)
|
||||||
|
|
||||||
|
// Mock repository calls
|
||||||
|
mockRepo.On("CreateOrderItems", ctx, orderID, mock.AnythingOfType("[]entity.OrderItem"), mockTx).Return(nil)
|
||||||
|
|
||||||
|
updatedOrder := &entity.Order{
|
||||||
|
ID: orderID,
|
||||||
|
Status: "pending",
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, Quantity: 2},
|
||||||
|
{ItemID: 2, Quantity: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockRepo.On("FindByID", ctx, orderID).Return(updatedOrder, nil)
|
||||||
|
|
||||||
|
// Mock calculation for updated totals
|
||||||
|
combinedItems := []entity.OrderItemRequest{
|
||||||
|
{ProductID: 1, Quantity: 2},
|
||||||
|
{ProductID: 2, Quantity: 1},
|
||||||
|
}
|
||||||
|
updatedCalculation := &entity.OrderCalculation{
|
||||||
|
Subtotal: 35.0,
|
||||||
|
Tax: 3.5,
|
||||||
|
Total: 38.5,
|
||||||
|
}
|
||||||
|
mockCalculator.On("CalculateOrderTotals", ctx, combinedItems, productDetails, updatedOrder.Source, updatedOrder.PartnerID).Return(updatedCalculation, nil)
|
||||||
|
|
||||||
|
updatedOrderWithTotals := &entity.Order{
|
||||||
|
ID: orderID,
|
||||||
|
Status: "pending",
|
||||||
|
Total: 38.5,
|
||||||
|
Tax: 3.5,
|
||||||
|
Amount: 35.0,
|
||||||
|
OrderItems: []entity.OrderItem{
|
||||||
|
{ItemID: 1, Quantity: 2},
|
||||||
|
{ItemID: 2, Quantity: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockRepo.On("CreateOrder", ctx, mock.AnythingOfType("*entity.Order"), mockTx).Return(updatedOrderWithTotals, nil)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
result, err := service.AddItems(ctx, orderID, newItems)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, orderID, result.ID)
|
||||||
|
|
||||||
|
// Verify all mocks were called
|
||||||
|
mockTrx.AssertExpectations(t)
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
mockCalculator.AssertExpectations(t)
|
||||||
|
mockProduct.AssertExpectations(t)
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"enaklo-pos-be/internal/common/mycontext"
|
"enaklo-pos-be/internal/common/mycontext"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -108,25 +109,61 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow voiding for NEW, PENDING orders
|
|
||||||
if order.Status != "NEW" && order.Status != "PENDING" {
|
if order.Status != "NEW" && order.Status != "PENDING" {
|
||||||
return errors.New("only new or pending orders can be voided")
|
return errors.New("only new or pending orders can be voided")
|
||||||
}
|
}
|
||||||
|
|
||||||
if voidType == "ALL" {
|
if voidType == "ALL" {
|
||||||
// Void entire order
|
// Void all items - create new VOIDED items for all existing items
|
||||||
|
for _, orderItem := range order.OrderItems {
|
||||||
|
if orderItem.Status == "ACTIVE" && orderItem.Quantity > 0 {
|
||||||
|
// Create new VOIDED order item with the voided quantity
|
||||||
|
voidedItem := &entity.OrderItem{
|
||||||
|
OrderID: orderID,
|
||||||
|
ItemID: orderItem.ItemID,
|
||||||
|
ItemType: orderItem.ItemType,
|
||||||
|
Price: orderItem.Price,
|
||||||
|
Quantity: orderItem.Quantity, // Void the full quantity
|
||||||
|
Status: "VOIDED",
|
||||||
|
CreatedBy: orderItem.CreatedBy,
|
||||||
|
ItemName: orderItem.ItemName,
|
||||||
|
Notes: reason, // Use the reason as notes for tracking
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.repo.CreateOrderItem(ctx, orderID, voidedItem)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to create voided order item", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update original item quantity to 0
|
||||||
|
err = s.repo.UpdateOrderItem(ctx, orderItem.ID, 0)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update original order item", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update order status to VOIDED
|
||||||
err = s.repo.UpdateOrder(ctx, orderID, "VOIDED", reason)
|
err = s.repo.UpdateOrder(ctx, orderID, "VOIDED", reason)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("failed to void order", zap.Error(err))
|
logger.ContextLogger(ctx).Error("failed to void order", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recalculate order totals (should be 0 for voided order)
|
||||||
|
err = s.repo.UpdateOrderTotals(ctx, orderID, 0, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
} else if voidType == "ITEM" {
|
} else if voidType == "ITEM" {
|
||||||
// Void specific items
|
// Void specific items
|
||||||
voidedAmount := 0.0
|
|
||||||
orderItemMap := make(map[int64]*entity.OrderItem)
|
orderItemMap := make(map[int64]*entity.OrderItem)
|
||||||
|
for i := range order.OrderItems {
|
||||||
for _, item := range order.OrderItems {
|
orderItemMap[order.OrderItems[i].ID] = &order.OrderItems[i]
|
||||||
orderItemMap[item.ID] = &item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, voidItem := range items {
|
for _, voidItem := range items {
|
||||||
@ -135,49 +172,94 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
|
|||||||
return errors.New(fmt.Sprintf("order item %d not found", voidItem.OrderItemID))
|
return errors.New(fmt.Sprintf("order item %d not found", voidItem.OrderItemID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if orderItem.Status != "ACTIVE" {
|
||||||
|
return errors.New(fmt.Sprintf("order item %d is not active", voidItem.OrderItemID))
|
||||||
|
}
|
||||||
|
|
||||||
if voidItem.Quantity > orderItem.Quantity {
|
if voidItem.Quantity > orderItem.Quantity {
|
||||||
return errors.New(fmt.Sprintf("void quantity %d exceeds available quantity %d for item %d",
|
return errors.New(fmt.Sprintf("void quantity %d exceeds available quantity %d for item %d",
|
||||||
voidItem.Quantity, orderItem.Quantity, voidItem.OrderItemID))
|
voidItem.Quantity, orderItem.Quantity, voidItem.OrderItemID))
|
||||||
}
|
}
|
||||||
|
|
||||||
voidedAmount += orderItem.Price * float64(voidItem.Quantity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update order items with reduced quantities
|
|
||||||
for _, voidItem := range items {
|
for _, voidItem := range items {
|
||||||
orderItem := orderItemMap[voidItem.OrderItemID]
|
orderItem := orderItemMap[voidItem.OrderItemID]
|
||||||
newQuantity := orderItem.Quantity - voidItem.Quantity
|
|
||||||
|
|
||||||
if newQuantity == 0 {
|
// Create new VOIDED order item with the voided quantity
|
||||||
// Remove item completely
|
voidedItem := &entity.OrderItem{
|
||||||
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, 0)
|
OrderID: orderID,
|
||||||
} else {
|
ItemID: orderItem.ItemID,
|
||||||
// Update quantity
|
ItemType: orderItem.ItemType,
|
||||||
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity)
|
Price: orderItem.Price,
|
||||||
|
Quantity: voidItem.Quantity, // Void the requested quantity
|
||||||
|
Status: "VOIDED",
|
||||||
|
CreatedBy: orderItem.CreatedBy,
|
||||||
|
ItemName: orderItem.ItemName,
|
||||||
|
Notes: reason, // Use the reason as notes for tracking
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = s.repo.CreateOrderItem(ctx, orderID, voidedItem)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to create voided order item", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update original item quantity
|
||||||
|
newQuantity := orderItem.Quantity - voidItem.Quantity
|
||||||
|
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err))
|
logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate order totals
|
updatedOrder, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||||
remainingAmount := order.Amount - voidedAmount
|
if err != nil {
|
||||||
remainingTax := (remainingAmount / order.Amount) * order.Tax
|
logger.ContextLogger(ctx).Error("failed to fetch updated order for recalculation", zap.Error(err))
|
||||||
remainingTotal := remainingAmount + remainingTax
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeItems []entity.OrderItemRequest
|
||||||
|
for _, item := range updatedOrder.OrderItems {
|
||||||
|
if item.Status == "ACTIVE" && item.Quantity > 0 {
|
||||||
|
activeItems = append(activeItems, entity.OrderItemRequest{
|
||||||
|
ProductID: item.ItemID,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
Notes: item.Notes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(activeItems) > 0 {
|
||||||
|
productIDs, _, err := s.ValidateOrderItems(ctx, activeItems)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to validate order items for recalculation", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to get product details for recalculation", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderCalculation, err := s.CalculateOrderTotals(ctx, activeItems, productDetails, order.Source, partnerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to calculate order totals", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Update order totals
|
// Update order totals
|
||||||
err = s.repo.UpdateOrderTotals(ctx, orderID, remainingAmount, remainingTax, remainingTotal)
|
err = s.repo.UpdateOrderTotals(ctx, orderID, orderCalculation.Subtotal, orderCalculation.Tax, orderCalculation.Total)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
|
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update order status to PARTIAL if some items remain, otherwise to VOIDED
|
// Update order status based on remaining amount
|
||||||
newStatus := "PARTIAL"
|
newStatus := "PENDING"
|
||||||
if remainingAmount <= 0 {
|
if orderCalculation.Subtotal <= 0 {
|
||||||
newStatus = "VOIDED"
|
newStatus = "CANCELED"
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason)
|
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason)
|
||||||
@ -185,6 +267,20 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
|
|||||||
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
|
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No active items left, cancel the order
|
||||||
|
err = s.repo.UpdateOrderTotals(ctx, orderID, 0, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.repo.UpdateOrder(ctx, orderID, "CANCELED", reason)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.ContextLogger(ctx).Info("order voided successfully",
|
logger.ContextLogger(ctx).Info("order voided successfully",
|
||||||
@ -195,8 +291,7 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SplitBillRequest handles splitting bills by items or amounts
|
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
|
||||||
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
|
|
||||||
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("failed to find order for split bill", zap.Error(err))
|
logger.ContextLogger(ctx).Error("failed to find order for split bill", zap.Error(err))
|
||||||
@ -210,9 +305,9 @@ func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID in
|
|||||||
var splitOrder *entity.Order
|
var splitOrder *entity.Order
|
||||||
|
|
||||||
if splitType == "ITEM" {
|
if splitType == "ITEM" {
|
||||||
splitOrder, err = s.splitByItems(ctx, order, paymentMethod, paymentProvider, items)
|
splitOrder, err = s.splitByItems(ctx, order, items)
|
||||||
} else if splitType == "AMOUNT" {
|
} else if splitType == "AMOUNT" {
|
||||||
splitOrder, err = s.splitByAmount(ctx, order, paymentMethod, paymentProvider, amount)
|
splitOrder, err = s.splitByAmount(ctx, order, amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -228,12 +323,12 @@ func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID in
|
|||||||
return splitOrder, nil
|
return splitOrder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, items []entity.SplitBillItem) (*entity.Order, error) {
|
func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Order, items []entity.SplitBillItem) (*entity.Order, error) {
|
||||||
var splitOrderItems []entity.OrderItem
|
var splitOrderItems []entity.OrderItem
|
||||||
orderItemMap := make(map[int64]*entity.OrderItem)
|
orderItemMap := make(map[int64]*entity.OrderItem)
|
||||||
|
|
||||||
for _, item := range originalOrder.OrderItems {
|
for i := range originalOrder.OrderItems {
|
||||||
orderItemMap[item.ID] = &item
|
orderItemMap[originalOrder.OrderItems[i].ID] = &originalOrder.OrderItems[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
assignedItems := make(map[int64]bool)
|
assignedItems := make(map[int64]bool)
|
||||||
@ -275,7 +370,6 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
|
|||||||
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
|
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
|
||||||
splitTotal := splitAmount + splitTax
|
splitTotal := splitAmount + splitTax
|
||||||
|
|
||||||
// Create new PAID order for the split
|
|
||||||
splitOrder := &entity.Order{
|
splitOrder := &entity.Order{
|
||||||
PartnerID: originalOrder.PartnerID,
|
PartnerID: originalOrder.PartnerID,
|
||||||
CustomerID: originalOrder.CustomerID,
|
CustomerID: originalOrder.CustomerID,
|
||||||
@ -284,8 +378,6 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
|
|||||||
Amount: splitAmount,
|
Amount: splitAmount,
|
||||||
Tax: splitTax,
|
Tax: splitTax,
|
||||||
Total: splitTotal,
|
Total: splitTotal,
|
||||||
PaymentType: paymentMethod,
|
|
||||||
PaymentProvider: paymentProvider,
|
|
||||||
Source: originalOrder.Source,
|
Source: originalOrder.Source,
|
||||||
CreatedBy: originalOrder.CreatedBy,
|
CreatedBy: originalOrder.CreatedBy,
|
||||||
OrderItems: splitOrderItems,
|
OrderItems: splitOrderItems,
|
||||||
@ -300,16 +392,13 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust original order items (reduce quantities)
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
orderItem := orderItemMap[item.OrderItemID]
|
orderItem := orderItemMap[item.OrderItemID]
|
||||||
newQuantity := orderItem.Quantity - item.Quantity
|
newQuantity := orderItem.Quantity - item.Quantity
|
||||||
|
|
||||||
if newQuantity == 0 {
|
if newQuantity == 0 {
|
||||||
// Remove item completely
|
|
||||||
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0)
|
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0)
|
||||||
} else {
|
} else {
|
||||||
// Update quantity
|
|
||||||
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, newQuantity)
|
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, newQuantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,12 +408,10 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate original order totals
|
|
||||||
remainingAmount := originalOrder.Amount - splitAmount
|
remainingAmount := originalOrder.Amount - splitAmount
|
||||||
remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax
|
remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax
|
||||||
remainingTotal := remainingAmount + remainingTax
|
remainingTotal := remainingAmount + remainingTax
|
||||||
|
|
||||||
// Update original order totals
|
|
||||||
err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal)
|
err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err))
|
logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err))
|
||||||
@ -335,7 +422,7 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
|
|||||||
}
|
}
|
||||||
|
|
||||||
// splitByAmount splits the order by assigning specific amounts to each split
|
// splitByAmount splits the order by assigning specific amounts to each split
|
||||||
func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, amount float64) (*entity.Order, error) {
|
func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, amount float64) (*entity.Order, error) {
|
||||||
// Validate that split amount is less than original order total
|
// Validate that split amount is less than original order total
|
||||||
if amount >= originalOrder.Total {
|
if amount >= originalOrder.Total {
|
||||||
return nil, errors.New(fmt.Sprintf("split amount %.2f must be less than order total %.2f",
|
return nil, errors.New(fmt.Sprintf("split amount %.2f must be less than order total %.2f",
|
||||||
@ -362,7 +449,6 @@ func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Or
|
|||||||
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
|
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
|
||||||
splitTotal := splitAmount + splitTax
|
splitTotal := splitAmount + splitTax
|
||||||
|
|
||||||
// Create new PAID order for the split
|
|
||||||
splitOrder := &entity.Order{
|
splitOrder := &entity.Order{
|
||||||
PartnerID: originalOrder.PartnerID,
|
PartnerID: originalOrder.PartnerID,
|
||||||
CustomerID: originalOrder.CustomerID,
|
CustomerID: originalOrder.CustomerID,
|
||||||
@ -371,8 +457,6 @@ func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Or
|
|||||||
Amount: splitAmount,
|
Amount: splitAmount,
|
||||||
Tax: splitTax,
|
Tax: splitTax,
|
||||||
Total: splitTotal,
|
Total: splitTotal,
|
||||||
PaymentType: paymentMethod,
|
|
||||||
PaymentProvider: paymentProvider,
|
|
||||||
Source: originalOrder.Source,
|
Source: originalOrder.Source,
|
||||||
CreatedBy: originalOrder.CreatedBy,
|
CreatedBy: originalOrder.CreatedBy,
|
||||||
OrderItems: splitOrderItems,
|
OrderItems: splitOrderItems,
|
||||||
|
|||||||
@ -15,24 +15,12 @@ type Repository interface {
|
|||||||
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
||||||
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
|
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
|
||||||
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
|
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
|
||||||
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error
|
||||||
GetOrderPaymentMethodBreakdown(
|
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
ctx mycontext.Context,
|
GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error)
|
||||||
partnerID int64,
|
GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error)
|
||||||
req entity.SearchRequest,
|
GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error)
|
||||||
) ([]entity.PaymentMethodBreakdown, error)
|
GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, 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)
|
|
||||||
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
|
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
|
||||||
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error)
|
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error)
|
||||||
@ -71,8 +59,8 @@ type Service interface {
|
|||||||
RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error
|
RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error
|
||||||
PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error
|
PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error
|
||||||
VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error
|
VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error
|
||||||
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error)
|
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error)
|
||||||
GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
GetOrderHistory(ctx mycontext.Context, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
CalculateOrderTotals(
|
CalculateOrderTotals(
|
||||||
ctx mycontext.Context,
|
ctx mycontext.Context,
|
||||||
items []entity.OrderItemRequest,
|
items []entity.OrderItemRequest,
|
||||||
@ -110,6 +98,7 @@ type Service interface {
|
|||||||
GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error)
|
GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error)
|
||||||
GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error)
|
GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error)
|
||||||
|
GetOrderByIDAndPartnerID(ctx mycontext.Context, orderID int64, partnerID int64) (*entity.Order, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config interface {
|
type Config interface {
|
||||||
|
|||||||
@ -4,12 +4,13 @@ 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"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) {
|
func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, request entity.SearchRequest) ([]*entity.Order, int64, error) {
|
||||||
return s.repo.GetOrderHistoryByPartnerID(ctx, partnerID, request)
|
return s.repo.GetOrderHistoryByPartnerID(ctx, ctx.GetPartnerID(), request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *orderSvc) GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) {
|
func (s *orderSvc) GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) {
|
||||||
@ -39,3 +40,16 @@ func (s *orderSvc) GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.O
|
|||||||
|
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *orderSvc) GetOrderByIDAndPartnerID(ctx mycontext.Context, orderID int64, partnerID int64) (*entity.Order, error) {
|
||||||
|
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to get order by ID and partner ID",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Int64("orderID", orderID),
|
||||||
|
zap.Int64("partnerID", partnerID))
|
||||||
|
return nil, errors.Wrap(err, "failed to get order")
|
||||||
|
}
|
||||||
|
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
-- Remove partner_id column from cashier_sessions table
|
||||||
|
ALTER TABLE cashier_sessions DROP COLUMN IF EXISTS partner_id;
|
||||||
|
|
||||||
|
-- Remove index
|
||||||
|
DROP INDEX IF EXISTS idx_cashier_sessions_partner_id;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
-- Add partner_id column to cashier_sessions table
|
||||||
|
ALTER TABLE cashier_sessions ADD COLUMN partner_id BIGINT NOT NULL DEFAULT 1;
|
||||||
|
|
||||||
|
-- Add index for better query performance
|
||||||
|
CREATE INDEX idx_cashier_sessions_partner_id ON cashier_sessions(partner_id);
|
||||||
|
|
||||||
|
-- Add foreign key constraint (assuming partners table exists)
|
||||||
|
-- ALTER TABLE cashier_sessions ADD CONSTRAINT fk_cashier_sessions_partner_id FOREIGN KEY (partner_id) REFERENCES partners(id);
|
||||||
Loading…
x
Reference in New Issue
Block a user