upodate system
This commit is contained in:
parent
1201b2e45b
commit
f31f83e485
@ -4,6 +4,7 @@ import "time"
|
||||
|
||||
type CashierSession struct {
|
||||
ID int64
|
||||
PartnerID int64
|
||||
CashierID int64
|
||||
OpenedAt time.Time
|
||||
ClosedAt *time.Time
|
||||
|
||||
@ -80,20 +80,20 @@ func (Order) TableName() string {
|
||||
}
|
||||
|
||||
type OrderItem struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement;column:order_item_id"`
|
||||
OrderID int64 `gorm:"type:int;column:order_id"`
|
||||
ItemID int64 `gorm:"type:int;column:item_id"`
|
||||
ItemType string `gorm:"type:varchar;column:item_type"`
|
||||
Price float64 `gorm:"type:numeric;not null;column:price"`
|
||||
Quantity int `gorm:"type:int;column:quantity"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
||||
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
||||
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
|
||||
Product *Product `gorm:"foreignKey:ItemID;references:ID"`
|
||||
ItemName string `gorm:"type:varchar;column:item_name"`
|
||||
Description string `gorm:"type:varchar;column:description"`
|
||||
Notes string `gorm:"type:varchar;column:notes"`
|
||||
ID int64 `gorm:"primaryKey;autoIncrement;column:order_item_id"`
|
||||
OrderID int64 `gorm:"type:int;column:order_id"`
|
||||
ItemID int64 `gorm:"type:int;column:item_id"`
|
||||
ItemType string `gorm:"type:varchar;column:item_type"`
|
||||
Price float64 `gorm:"type:numeric;not null;column:price"`
|
||||
Quantity int `gorm:"type:int;column:quantity"`
|
||||
Status string `gorm:"type:varchar;column:status;default:ACTIVE"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
||||
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
||||
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
|
||||
Product *Product `gorm:"foreignKey:ItemID;references:ID"`
|
||||
ItemName string `gorm:"type:varchar;column:item_name"`
|
||||
Notes string `gorm:"type:varchar;column:notes"`
|
||||
}
|
||||
|
||||
func (OrderItem) TableName() string {
|
||||
@ -135,10 +135,10 @@ type VoidItem struct {
|
||||
}
|
||||
|
||||
type SplitBillSplit struct {
|
||||
CustomerName string `json:"customer_name" validate:"required"`
|
||||
CustomerID *int64 `json:"customer_id"`
|
||||
Items []SplitBillItem `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
|
||||
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
|
||||
CustomerName string `json:"customer_name" validate:"required"`
|
||||
CustomerID *int64 `json:"customer_id"`
|
||||
Items []SplitBillItem `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
|
||||
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
|
||||
}
|
||||
|
||||
type SplitBillItem struct {
|
||||
|
||||
@ -5,9 +5,10 @@ import (
|
||||
"enaklo-pos-be/internal/handlers/request"
|
||||
"enaklo-pos-be/internal/handlers/response"
|
||||
"enaklo-pos-be/internal/services/v2/cashier_session"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CashierSessionHandler struct {
|
||||
@ -15,7 +16,9 @@ type CashierSessionHandler struct {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -26,6 +29,7 @@ func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFun
|
||||
route.POST("/close/:id", h.CloseSession)
|
||||
route.GET("/open", h.GetOpenSession)
|
||||
route.GET("/report/:id", h.GetSessionReport)
|
||||
route.GET("/history", h.GetSessionHistory)
|
||||
}
|
||||
|
||||
func (h *CashierSessionHandler) OpenSession(c *gin.Context) {
|
||||
@ -121,3 +125,45 @@ func (h *CashierSessionHandler) GetSessionReport(c *gin.Context) {
|
||||
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("/detail/:id", jwt, h.GetOrderID)
|
||||
|
||||
}
|
||||
|
||||
func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
|
||||
@ -99,6 +98,7 @@ func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
|
||||
Price: item.Price,
|
||||
Quantity: 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/response"
|
||||
"enaklo-pos-be/internal/services/v2/order"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service order.Service
|
||||
service order.Service
|
||||
queryParser *request.QueryParser
|
||||
}
|
||||
|
||||
func NewOrderHandler(service order.Service) *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("/sales-by-category", jwt, h.GetSalesByCategory)
|
||||
route.GET("/popular-products", jwt, h.GetPopularProducts)
|
||||
route.GET("/detail/:id", jwt, h.GetByID)
|
||||
}
|
||||
|
||||
type InquiryRequest struct {
|
||||
@ -104,12 +108,10 @@ type VoidItemRequest struct {
|
||||
}
|
||||
|
||||
type SplitBillRequest struct {
|
||||
OrderID int64 `json:"order_id" validate:"required"`
|
||||
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"`
|
||||
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
|
||||
OrderID int64 `json:"order_id" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"`
|
||||
Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
|
||||
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
|
||||
}
|
||||
|
||||
type SplitBillItemRequest struct {
|
||||
@ -318,7 +320,7 @@ func (h *Handler) Refund(c *gin.Context) {
|
||||
Reason: req.Reason,
|
||||
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
CustomerName: order.CustomerName,
|
||||
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
||||
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
@ -330,116 +332,30 @@ func (h *Handler) Refund(c *gin.Context) {
|
||||
|
||||
func (h *Handler) GetOrderHistory(c *gin.Context) {
|
||||
ctx := request.GetMyContext(c)
|
||||
partnerID := ctx.GetPartnerID()
|
||||
|
||||
limitStr := c.Query("limit")
|
||||
offsetStr := c.Query("offset")
|
||||
status := c.Query("status")
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
|
||||
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)
|
||||
searchReq, err := h.queryParser.ParseSearchRequest(c)
|
||||
if err != nil {
|
||||
response.ErrorWrapper(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseData := []response.OrderHistoryResponse{}
|
||||
for _, order := range orders {
|
||||
var orderItems []response.OrderItemResponse
|
||||
for _, item := range order.OrderItems {
|
||||
orderItems = append(orderItems, response.OrderItemResponse{
|
||||
ProductID: item.ItemID,
|
||||
ProductName: item.ItemName,
|
||||
Price: item.Price,
|
||||
Quantity: item.Quantity,
|
||||
Subtotal: item.Price * float64(item.Quantity),
|
||||
})
|
||||
}
|
||||
|
||||
responseData = append(responseData, response.OrderHistoryResponse{
|
||||
ID: order.ID,
|
||||
CustomerName: order.CustomerName,
|
||||
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,
|
||||
})
|
||||
orders, total, err := h.service.GetOrderHistory(ctx, *searchReq)
|
||||
if err != nil {
|
||||
response.ErrorWrapper(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
responseData := response.MapOrderHistoryResponse(orders)
|
||||
pagingMeta := response.NewPaginationHelper().BuildPagingMeta(searchReq.Offset, searchReq.Limit, total)
|
||||
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: responseData,
|
||||
PagingMeta: &response.PagingMeta{
|
||||
Page: offset + 1,
|
||||
Total: int64(total),
|
||||
Limit: limit,
|
||||
},
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: responseData,
|
||||
PagingMeta: pagingMeta,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) formatPayment(payment, provider string) string {
|
||||
if payment == "CASH" {
|
||||
return payment
|
||||
}
|
||||
|
||||
return payment + " " + provider
|
||||
}
|
||||
|
||||
func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) {
|
||||
ctx := request.GetMyContext(c)
|
||||
partnerID := ctx.GetPartnerID()
|
||||
@ -501,7 +417,7 @@ func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) {
|
||||
paymentBreakdown := make([]PaymentMethodBreakdown, len(paymentAnalysis.PaymentMethodBreakdown))
|
||||
for i, bd := range paymentAnalysis.PaymentMethodBreakdown {
|
||||
paymentBreakdown[i] = PaymentMethodBreakdown{
|
||||
PaymentMethod: h.formatPayment(bd.PaymentType, bd.PaymentProvider),
|
||||
PaymentMethod: response.NewPaymentFormatter().Format(bd.PaymentType, bd.PaymentProvider),
|
||||
TotalTransactions: bd.TotalTransactions,
|
||||
TotalAmount: bd.TotalAmount,
|
||||
}
|
||||
@ -632,7 +548,6 @@ func (h *Handler) GetPopularProducts(c *gin.Context) {
|
||||
|
||||
func (h *Handler) GetRefundHistory(c *gin.Context) {
|
||||
ctx := request.GetMyContext(c)
|
||||
partnerID := ctx.GetPartnerID()
|
||||
|
||||
limitStr := c.Query("limit")
|
||||
offsetStr := c.Query("offset")
|
||||
@ -664,8 +579,6 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
|
||||
}
|
||||
|
||||
searchReq.Offset = offset
|
||||
|
||||
// Set status to REFUNDED to get only refunded orders
|
||||
searchReq.Status = "REFUNDED"
|
||||
|
||||
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 {
|
||||
response.ErrorWrapper(c, err)
|
||||
return
|
||||
@ -698,7 +611,7 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
|
||||
Status: order.Status,
|
||||
Amount: order.Amount,
|
||||
Total: order.Total,
|
||||
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
||||
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
|
||||
TableNumber: order.TableNumber,
|
||||
OrderType: order.OrderType,
|
||||
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
@ -759,7 +672,6 @@ func (h *Handler) PartialRefund(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate refunded amount
|
||||
refundedAmount := 0.0
|
||||
var refundedItems []RefundedItemResponse
|
||||
|
||||
@ -789,7 +701,7 @@ func (h *Handler) PartialRefund(c *gin.Context) {
|
||||
Reason: req.Reason,
|
||||
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
CustomerName: order.CustomerName,
|
||||
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
||||
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
|
||||
RefundedItems: refundedItems,
|
||||
}
|
||||
|
||||
@ -815,7 +727,6 @@ func (h *Handler) VoidOrder(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert request items to entity items
|
||||
var items []entity.VoidItem
|
||||
if req.Type == "ITEM" {
|
||||
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 {
|
||||
response.ErrorWrapper(c, err)
|
||||
return
|
||||
@ -918,3 +830,26 @@ func (h *Handler) SplitBill(c *gin.Context) {
|
||||
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"
|
||||
|
||||
type OpenCashierSessionRequest struct {
|
||||
PartnerID int64 `json:"partner_id" validate:"required"`
|
||||
OpeningAmount float64 `json:"opening_amount" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
@ -12,6 +13,7 @@ type CloseCashierSessionRequest struct {
|
||||
|
||||
func (o *OpenCashierSessionRequest) ToEntity(cashierID int64) *entity.CashierSession {
|
||||
return &entity.CashierSession{
|
||||
PartnerID: o.PartnerID,
|
||||
CashierID: cashierID,
|
||||
OpeningAmount: o.OpeningAmount,
|
||||
}
|
||||
|
||||
@ -30,19 +30,17 @@ func (p *ProductParam) ToEntity(partnerID int64) entity.ProductSearch {
|
||||
}
|
||||
|
||||
type Product struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
PartnerID int64 `json:"partner_id"`
|
||||
SiteID int64 `json:"site_id"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type string `json:"type"`
|
||||
Price float64 `json:"price" validate:"required"`
|
||||
IsWeekendTicket bool `json:"is_weekend_ticket"`
|
||||
IsSeasonTicket bool `json:"is_season_ticket"`
|
||||
Status string `json:"status"`
|
||||
Description string `json:"description"`
|
||||
Stock int64 `json:"stock"`
|
||||
Image string `json:"image"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
PartnerID int64 `json:"partner_id"`
|
||||
SiteID int64 `json:"site_id"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type string `json:"type"`
|
||||
Price float64 `json:"price" validate:"required"`
|
||||
Status string `json:"status"`
|
||||
Description string `json:"description"`
|
||||
Stock int64 `json:"stock"`
|
||||
Image string `json:"image"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
}
|
||||
|
||||
func (e *Product) ToEntity() *entity.Product {
|
||||
|
||||
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 {
|
||||
ID int64 `json:"id"`
|
||||
PartnerID int64 `json:"partner_id"`
|
||||
CashierID int64 `json:"cashier_id"`
|
||||
OpenedAt time.Time `json:"opened_at"`
|
||||
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
||||
@ -35,6 +36,7 @@ func MapToCashierSessionResponse(e *entity.CashierSession) *CashierSessionRespon
|
||||
|
||||
return &CashierSessionResponse{
|
||||
ID: e.ID,
|
||||
PartnerID: e.PartnerID,
|
||||
CashierID: e.CashierID,
|
||||
OpenedAt: e.OpenedAt,
|
||||
ClosedAt: e.ClosedAt,
|
||||
|
||||
@ -3,6 +3,7 @@ package response
|
||||
import (
|
||||
"enaklo-pos-be/internal/constants/order"
|
||||
"enaklo-pos-be/internal/constants/transaction"
|
||||
"enaklo-pos-be/internal/entity"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -204,3 +205,52 @@ type OrderHistoryResponse struct {
|
||||
Tax float64 `json:"tax"`
|
||||
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 {
|
||||
OrderItemID int64 `json:"order_item_id"`
|
||||
ProductID int64 `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Price float64 `json:"price"`
|
||||
Quantity int `json:"quantity"`
|
||||
Subtotal float64 `json:"subtotal"`
|
||||
Notes string `json:"notes"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func mapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
|
||||
@ -109,12 +111,14 @@ func MapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
|
||||
result := make([]OrderItemResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, 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 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
|
||||
|
||||
import (
|
||||
"enaklo-pos-be/internal/common/logger"
|
||||
"enaklo-pos-be/internal/common/mycontext"
|
||||
"enaklo-pos-be/internal/entity"
|
||||
"enaklo-pos-be/internal/repository/models"
|
||||
"enaklo-pos-be/internal/services/v2/inprogress_order"
|
||||
time2 "time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
time2 "time"
|
||||
)
|
||||
|
||||
type InProgressOrderRepository interface {
|
||||
CreateOrUpdate(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error)
|
||||
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error)
|
||||
FindByID(ctx mycontext.Context, id int64) (*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)
|
||||
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
|
||||
}
|
||||
|
||||
type inprogressOrderRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewInProgressOrderRepository(db *gorm.DB) *inprogressOrderRepository {
|
||||
func NewInProgressOrderRepository(db *gorm.DB) inprogress_order.OrderRepository {
|
||||
return &inprogressOrderRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *inprogressOrderRepository) CreateOrUpdate(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) {
|
||||
isUpdate := order.ID != ""
|
||||
func (r *inprogressOrderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) {
|
||||
var orderDB models.OrderDB
|
||||
|
||||
tx := r.db.Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, errors.Wrap(tx.Error, "failed to begin transaction")
|
||||
if err := r.db.Preload("OrderItems").First(&orderDB, id).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")
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
orderDB := r.toInProgressOrderDBModel(order)
|
||||
order := r.toDomainOrderModel(&orderDB)
|
||||
|
||||
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")
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
if err := tx.Model(&orderDB).Updates(orderDB).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.Wrap(err, "failed to update order")
|
||||
}
|
||||
func (r *inprogressOrderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
|
||||
orderDB := r.toOrderDBModel(order)
|
||||
|
||||
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")
|
||||
}
|
||||
// Use provided transaction or create new one
|
||||
var dbTx *gorm.DB
|
||||
if tx != nil {
|
||||
dbTx = tx
|
||||
} else {
|
||||
if err := tx.Create(&orderDB).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.Wrap(err, "failed to insert order")
|
||||
dbTx = r.db.Begin()
|
||||
if dbTx.Error != nil {
|
||||
return nil, errors.Wrap(dbTx.Error, "failed to begin transaction")
|
||||
}
|
||||
|
||||
order.ID = orderDB.ID
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
dbTx.Rollback()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var itemIDs []int64
|
||||
for i := range order.OrderItems {
|
||||
itemIDs = append(itemIDs, order.OrderItems[i].ItemID)
|
||||
}
|
||||
|
||||
var products []models.ProductDB
|
||||
if len(itemIDs) > 0 {
|
||||
if err := tx.Where("id IN ?", itemIDs).Find(&products).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.Wrap(err, "failed to fetch products")
|
||||
if err := dbTx.Create(&orderDB).Error; err != nil {
|
||||
if tx == nil {
|
||||
dbTx.Rollback()
|
||||
}
|
||||
return nil, errors.Wrap(err, "failed to insert order")
|
||||
}
|
||||
order.ID = orderDB.ID
|
||||
|
||||
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")
|
||||
// Only commit if we created the transaction
|
||||
if tx == nil {
|
||||
if err := dbTx.Commit().Error; err != nil {
|
||||
return nil, errors.Wrap(err, "failed to commit transaction")
|
||||
}
|
||||
|
||||
item.ID = itemDB.ID
|
||||
|
||||
if product, exists := productMap[item.ItemID]; exists {
|
||||
item.Product = r.toDomainProductModel(&product)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, errors.Wrap(err, "failed to commit transaction")
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) {
|
||||
var ordersDB []models.InProgressOrderDB
|
||||
query := r.db.Where("partner_id = ?", partnerID).Order("created_at DESC")
|
||||
func (r *inprogressOrderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
|
||||
if len(items) == 0 {
|
||||
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 {
|
||||
query = query.Limit(limit)
|
||||
@ -116,27 +120,39 @@ func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, pa
|
||||
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")
|
||||
}
|
||||
|
||||
orders := make([]*entity.InProgressOrder, 0, len(ordersDB))
|
||||
orders := make([]*entity.Order, 0, len(ordersDB))
|
||||
for _, orderDB := range ordersDB {
|
||||
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)
|
||||
|
||||
orderItem := entity.InProgressOrderItem{
|
||||
orderItem := entity.OrderItem{
|
||||
ID: item.ID,
|
||||
ItemID: item.ItemID,
|
||||
Quantity: item.Quantity,
|
||||
ItemName: item.ItemName,
|
||||
}
|
||||
|
||||
if itemDB.Product.ID > 0 {
|
||||
productDomain := r.toDomainProductModel(&itemDB.Product)
|
||||
orderItem.Product = productDomain
|
||||
if itemDB.ItemID > 0 {
|
||||
var product models.ProductDB
|
||||
err := r.db.First(&product, itemDB.ItemID).Error
|
||||
|
||||
if err == nil {
|
||||
productDomain := r.toDomainProductModel(&product)
|
||||
orderItem.Product = productDomain
|
||||
}
|
||||
}
|
||||
|
||||
order.OrderItems = append(order.OrderItems, orderItem)
|
||||
@ -148,106 +164,122 @@ func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, pa
|
||||
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()
|
||||
return models.InProgressOrderDB{
|
||||
ID: order.ID,
|
||||
PartnerID: order.PartnerID,
|
||||
CustomerID: order.CustomerID,
|
||||
CustomerName: order.CustomerName,
|
||||
PaymentType: order.PaymentType,
|
||||
CreatedBy: order.CreatedBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
TableNumber: order.TableNumber,
|
||||
OrderType: order.OrderType,
|
||||
return models.OrderDB{
|
||||
ID: order.ID,
|
||||
PartnerID: order.PartnerID,
|
||||
CustomerID: order.CustomerID,
|
||||
CustomerName: order.CustomerName,
|
||||
PaymentType: order.PaymentType,
|
||||
PaymentProvider: order.PaymentProvider,
|
||||
CreatedBy: order.CreatedBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
TableNumber: order.TableNumber,
|
||||
OrderType: order.OrderType,
|
||||
Status: order.Status,
|
||||
Amount: order.Amount,
|
||||
Total: order.Total,
|
||||
Tax: order.Tax,
|
||||
Source: order.Source,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *inprogressOrderRepository) toDomainOrderModel(dbModel *models.InProgressOrderDB) *entity.InProgressOrder {
|
||||
return &entity.InProgressOrder{
|
||||
ID: dbModel.ID,
|
||||
PartnerID: dbModel.PartnerID,
|
||||
CustomerID: dbModel.CustomerID,
|
||||
CustomerName: dbModel.CustomerName,
|
||||
PaymentType: dbModel.PaymentType,
|
||||
CreatedBy: dbModel.CreatedBy,
|
||||
OrderItems: []entity.InProgressOrderItem{},
|
||||
TableNumber: dbModel.TableNumber,
|
||||
OrderType: dbModel.OrderType,
|
||||
CreatedAt: dbModel.CreatedAt,
|
||||
UpdatedAt: dbModel.UpdatedAt,
|
||||
func (r *inprogressOrderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order {
|
||||
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,
|
||||
PartnerID: dbModel.PartnerID,
|
||||
CustomerID: dbModel.CustomerID,
|
||||
InquiryID: dbModel.InquiryID,
|
||||
Status: dbModel.Status,
|
||||
Amount: dbModel.Amount,
|
||||
Tax: dbModel.Tax,
|
||||
Total: dbModel.Total,
|
||||
PaymentType: dbModel.PaymentType,
|
||||
Source: dbModel.Source,
|
||||
CreatedBy: dbModel.CreatedBy,
|
||||
CreatedAt: dbModel.CreatedAt,
|
||||
UpdatedAt: dbModel.UpdatedAt,
|
||||
OrderItems: orderItems,
|
||||
CustomerName: dbModel.CustomerName,
|
||||
TableNumber: dbModel.TableNumber,
|
||||
OrderType: dbModel.OrderType,
|
||||
PaymentProvider: dbModel.PaymentProvider,
|
||||
}
|
||||
}
|
||||
|
||||
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) toOrderItemDBModel(item *entity.OrderItem) models.OrderItemDB {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *inprogressOrderRepository) toDomainOrderItemModel(dbModel *models.InProgressOrderItemDB) *entity.OrderItem {
|
||||
func (r *inprogressOrderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *entity.OrderItem {
|
||||
return &entity.OrderItem{
|
||||
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) 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,
|
||||
Amount: dbModel.Amount,
|
||||
Tax: dbModel.Tax,
|
||||
Total: dbModel.Total,
|
||||
PaymentType: dbModel.PaymentType,
|
||||
Source: dbModel.Source,
|
||||
CreatedBy: dbModel.CreatedBy,
|
||||
CreatedAt: dbModel.CreatedAt,
|
||||
ExpiresAt: dbModel.ExpiresAt,
|
||||
OrderItems: []entity.OrderItem{},
|
||||
}
|
||||
|
||||
if dbModel.CustomerID != nil {
|
||||
inquiry.CustomerID = *dbModel.CustomerID
|
||||
}
|
||||
|
||||
inquiry.UpdatedAt = dbModel.UpdatedAt
|
||||
|
||||
return inquiry
|
||||
}
|
||||
|
||||
func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product {
|
||||
if productDB == nil {
|
||||
return nil
|
||||
@ -264,3 +296,26 @@ func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.Produ
|
||||
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/entity"
|
||||
"enaklo-pos-be/internal/repository/models"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CashierSessionRepository interface {
|
||||
@ -15,6 +16,7 @@ type CashierSessionRepository interface {
|
||||
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
|
||||
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, 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 {
|
||||
@ -27,6 +29,7 @@ func NewCashierSessionRepository(db *gorm.DB) CashierSessionRepository {
|
||||
|
||||
func (r *cashierSessionRepository) CreateSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) {
|
||||
dbModel := models.CashierSessionDB{
|
||||
PartnerID: session.PartnerID,
|
||||
CashierID: session.CashierID,
|
||||
OpenedAt: time.Now(),
|
||||
OpeningAmount: session.OpeningAmount,
|
||||
@ -94,6 +97,7 @@ func (r *cashierSessionRepository) GetSessionByID(ctx mycontext.Context, session
|
||||
func (r *cashierSessionRepository) toEntity(db *models.CashierSessionDB) *entity.CashierSession {
|
||||
return &entity.CashierSession{
|
||||
ID: db.ID,
|
||||
PartnerID: db.PartnerID,
|
||||
CashierID: db.CashierID,
|
||||
OpenedAt: db.OpenedAt,
|
||||
ClosedAt: db.ClosedAt,
|
||||
@ -135,3 +139,39 @@ func (r *cashierSessionRepository) GetPaymentSummaryBySessionID(ctx mycontext.Co
|
||||
|
||||
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 {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
PartnerID int64 `gorm:"not null"`
|
||||
CashierID int64 `gorm:"not null"`
|
||||
OpenedAt time.Time `gorm:"not null"`
|
||||
ClosedAt *time.Time
|
||||
|
||||
@ -39,6 +39,7 @@ type OrderItemDB struct {
|
||||
ItemType string `gorm:"column:item_type"`
|
||||
Price float64 `gorm:"column:price"`
|
||||
Quantity int `gorm:"column:quantity"`
|
||||
Status string `gorm:"column:status;default:ACTIVE"`
|
||||
CreatedBy int64 `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
Product ProductDB `gorm:"foreignKey:ItemID;references:ID"`
|
||||
|
||||
@ -5,10 +5,11 @@ import (
|
||||
"enaklo-pos-be/internal/common/mycontext"
|
||||
"enaklo-pos-be/internal/entity"
|
||||
"enaklo-pos-be/internal/repository/models"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OrderRepository interface {
|
||||
@ -17,32 +18,22 @@ type OrderRepository interface {
|
||||
CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error)
|
||||
FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error)
|
||||
UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error
|
||||
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||
CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error)
|
||||
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, 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)
|
||||
GetOrderPaymentMethodBreakdown(
|
||||
ctx mycontext.Context,
|
||||
partnerID int64,
|
||||
req entity.SearchRequest,
|
||||
) ([]entity.PaymentMethodBreakdown, error)
|
||||
GetRevenueOverview(
|
||||
ctx mycontext.Context,
|
||||
req entity.RevenueOverviewRequest,
|
||||
) ([]entity.RevenueOverviewItem, error)
|
||||
GetSalesByCategory(
|
||||
ctx mycontext.Context,
|
||||
req entity.SalesByCategoryRequest,
|
||||
) ([]entity.SalesByCategoryItem, error)
|
||||
GetPopularProducts(
|
||||
ctx mycontext.Context,
|
||||
req entity.PopularProductsRequest,
|
||||
) ([]entity.PopularProductItem, error)
|
||||
GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error)
|
||||
GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error)
|
||||
GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error)
|
||||
GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, error)
|
||||
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, 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)
|
||||
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
||||
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) 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 {
|
||||
@ -129,11 +120,6 @@ func (r *orderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Ord
|
||||
|
||||
order := r.toDomainOrderModel(&orderDB)
|
||||
|
||||
for _, itemDB := range orderDB.OrderItems {
|
||||
item := r.toDomainOrderItemModel(&itemDB)
|
||||
order.OrderItems = append(order.OrderItems, *item)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems))
|
||||
for _, itemDB := range dbModel.OrderItems {
|
||||
orderItems = append(orderItems, entity.OrderItem{
|
||||
ID: itemDB.ID,
|
||||
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,
|
||||
@ -327,6 +314,7 @@ func (r *orderRepository) toOrderItemDBModel(item *entity.OrderItem) models.Orde
|
||||
ItemName: item.ItemName,
|
||||
Price: item.Price,
|
||||
Quantity: item.Quantity,
|
||||
Status: item.Status,
|
||||
CreatedBy: item.CreatedBy,
|
||||
CreatedAt: item.CreatedAt,
|
||||
Notes: item.Notes,
|
||||
@ -341,9 +329,11 @@ func (r *orderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *e
|
||||
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,
|
||||
@ -406,65 +396,70 @@ func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiry
|
||||
return inquiry
|
||||
}
|
||||
|
||||
func (r *orderRepository) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) {
|
||||
var ordersDB []models.OrderDB
|
||||
var totalCount int64
|
||||
func (r *orderRepository) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error) {
|
||||
queryBuilder := NewQueryBuilder[models.OrderDB](r.db)
|
||||
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 != "" {
|
||||
baseQuery = baseQuery.Where("status = ?", req.Status)
|
||||
filters = append(filters, Equal("status", req.Status))
|
||||
}
|
||||
|
||||
if !req.Start.IsZero() {
|
||||
baseQuery = baseQuery.Where("created_at >= ?", req.Start)
|
||||
filters = append(filters, GreaterEqual("created_at", req.Start))
|
||||
}
|
||||
|
||||
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
|
||||
if err := baseQuery.Count(&totalCount).Error; err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to count total orders")
|
||||
options := QueryOptions{
|
||||
Filters: filters,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
OrderBy: []string{"created_at DESC"},
|
||||
Preloads: []string{"OrderItems"},
|
||||
}
|
||||
|
||||
// Clone the query for fetching the actual data with pagination
|
||||
query := baseQuery.Session(&gorm.Session{})
|
||||
|
||||
// Add ordering and pagination
|
||||
query = query.Order("created_at DESC")
|
||||
|
||||
if req.Limit > 0 {
|
||||
query = query.Limit(req.Limit)
|
||||
baseQuery := queryBuilder.BuildQuery(options)
|
||||
totalCount, err := queryBuilder.Count(baseQuery)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if req.Offset > 0 {
|
||||
query = query.Offset(req.Offset)
|
||||
query := queryBuilder.ExecuteQuery(baseQuery, options)
|
||||
ordersDB, err := queryBuilder.Find(query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Execute the query with preloading
|
||||
if err := query.Preload("OrderItems").Find(&ordersDB).Error; err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to find order history by partner ID")
|
||||
}
|
||||
orders := r.convertOrdersToEntity(ordersDB)
|
||||
|
||||
// Map to domain models
|
||||
return orders, totalCount, nil
|
||||
}
|
||||
|
||||
func (r *orderRepository) convertOrdersToEntity(ordersDB []models.OrderDB) []*entity.Order {
|
||||
orders := make([]*entity.Order, 0, len(ordersDB))
|
||||
|
||||
for _, orderDB := range ordersDB {
|
||||
order := r.toDomainOrderModel(&orderDB)
|
||||
order.OrderItems = make([]entity.OrderItem, 0, len(orderDB.OrderItems))
|
||||
|
||||
for _, itemDB := range orderDB.OrderItems {
|
||||
item := r.toDomainOrderItemModel(&itemDB)
|
||||
order.OrderItems = append(order.OrderItems, *item)
|
||||
}
|
||||
|
||||
order.OrderItems = r.convertOrderItemsToEntity(orderDB.OrderItems)
|
||||
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) {
|
||||
@ -521,126 +516,108 @@ func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID
|
||||
return orders, totalCount, nil
|
||||
}
|
||||
|
||||
func (r *orderRepository) CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) {
|
||||
isUpdate := order.ID != 0
|
||||
func (r *orderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
|
||||
orderDB := r.toOrderDBModel(order)
|
||||
|
||||
tx := r.db.Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, errors.Wrap(tx.Error, "failed to begin transaction")
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
orderDB := r.toInProgressOrderDBModel(order)
|
||||
|
||||
if isUpdate {
|
||||
var existingOrder models.OrderDB
|
||||
if err := tx.First(&existingOrder, order.ID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.Wrap(err, "order not found for update")
|
||||
}
|
||||
|
||||
if err := tx.Model(&orderDB).Updates(orderDB).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.Wrap(err, "failed to update order")
|
||||
}
|
||||
|
||||
if err := tx.Where("order_id = ?", order.ID).Delete(&models.OrderItemDB{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.Wrap(err, "failed to delete existing order items")
|
||||
}
|
||||
// Use provided transaction or create new one
|
||||
var dbTx *gorm.DB
|
||||
if tx != nil {
|
||||
dbTx = tx
|
||||
} else {
|
||||
if err := tx.Create(&orderDB).Error; err != nil {
|
||||
tx.Rollback()
|
||||
dbTx = r.db.Begin()
|
||||
if dbTx.Error != nil {
|
||||
return nil, errors.Wrap(dbTx.Error, "failed to begin transaction")
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
dbTx.Rollback()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if order.InProgressOrderID != 0 {
|
||||
// Update existing 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")
|
||||
}
|
||||
order.ID = order.InProgressOrderID
|
||||
} else {
|
||||
// Create new order
|
||||
if err := dbTx.Create(&orderDB).Error; err != nil {
|
||||
if tx == nil {
|
||||
dbTx.Rollback()
|
||||
}
|
||||
return nil, errors.Wrap(err, "failed to insert order")
|
||||
}
|
||||
|
||||
order.ID = orderDB.ID
|
||||
}
|
||||
|
||||
var itemIDs []int64
|
||||
for i := range order.OrderItems {
|
||||
itemIDs = append(itemIDs, order.OrderItems[i].ItemID)
|
||||
}
|
||||
|
||||
var products []models.ProductDB
|
||||
if len(itemIDs) > 0 {
|
||||
if err := tx.Where("id IN ?", itemIDs).Find(&products).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.Wrap(err, "failed to fetch products")
|
||||
// Only commit if we created the transaction
|
||||
if tx == nil {
|
||||
if err := dbTx.Commit().Error; err != nil {
|
||||
return nil, errors.Wrap(err, "failed to commit transaction")
|
||||
}
|
||||
}
|
||||
|
||||
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 the order with the ID set, but without items (items will be added separately)
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (r *orderRepository) toInProgressOrderDBModel(order *entity.Order) models.OrderDB {
|
||||
now := time.Now()
|
||||
|
||||
return models.OrderDB{
|
||||
ID: order.ID,
|
||||
PartnerID: order.PartnerID,
|
||||
CustomerID: order.CustomerID,
|
||||
CustomerName: order.CustomerName,
|
||||
PaymentType: order.PaymentType,
|
||||
PaymentProvider: order.PaymentProvider,
|
||||
CreatedBy: order.CreatedBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
TableNumber: order.TableNumber,
|
||||
OrderType: order.OrderType,
|
||||
Status: order.Status,
|
||||
Amount: order.Amount,
|
||||
Total: order.Total,
|
||||
Tax: order.Tax,
|
||||
Source: order.Source,
|
||||
func (r *orderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
|
||||
// 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 errors.Wrap(dbTx.Error, "failed to begin transaction")
|
||||
}
|
||||
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 {
|
||||
if productDB == nil {
|
||||
return nil
|
||||
func (r *orderRepository) CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error {
|
||||
itemDB := r.toOrderItemDBModel(item)
|
||||
itemDB.OrderID = orderID
|
||||
|
||||
if err := r.db.Create(&itemDB).Error; err != nil {
|
||||
return errors.Wrap(err, "failed to insert order item")
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
item.ID = itemDB.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
for _, itemDB := range orderDB.OrderItems {
|
||||
item := r.toDomainOrderItemModel(&itemDB)
|
||||
order.OrderItems = append(order.OrderItems, *item)
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
@ -1025,3 +997,43 @@ func (r *orderRepository) UpdateOrderTotals(ctx mycontext.Context, orderID int64
|
||||
|
||||
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/sites"
|
||||
transactions "enaklo-pos-be/internal/repository/transaction"
|
||||
"enaklo-pos-be/internal/repository/trx"
|
||||
"enaklo-pos-be/internal/repository/users"
|
||||
repository "enaklo-pos-be/internal/repository/wallet"
|
||||
|
||||
@ -38,7 +37,7 @@ type RepoManagerImpl struct {
|
||||
OSS OSSRepository
|
||||
Partner PartnerRepository
|
||||
Site SiteRepository
|
||||
Trx TransactionManager
|
||||
Trx Trx
|
||||
Wallet WalletRepository
|
||||
Midtrans Midtrans
|
||||
Payment Payment
|
||||
@ -70,7 +69,7 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
|
||||
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
|
||||
Partner: partners.NewPartnerRepository(db),
|
||||
Site: sites.NewSiteRepository(db),
|
||||
Trx: trx.NewGormTransactionManager(db),
|
||||
Trx: NewTransactionManager(db),
|
||||
Wallet: repository.NewWalletRepository(db),
|
||||
Midtrans: mdtrns.New(&cfg.Midtrans),
|
||||
Payment: payment.NewPaymentRepository(db),
|
||||
@ -188,12 +187,6 @@ type SiteRepository interface {
|
||||
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 {
|
||||
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)
|
||||
@ -244,3 +237,9 @@ type PaymentGateway interface {
|
||||
CreateQRISPayment(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"
|
||||
"enaklo-pos-be/internal/handlers/http/transaction"
|
||||
"enaklo-pos-be/internal/handlers/http/user"
|
||||
"net/http"
|
||||
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"net/http"
|
||||
|
||||
"enaklo-pos-be/internal/middlewares"
|
||||
|
||||
|
||||
@ -20,13 +20,13 @@ type AuthServiceImpl struct {
|
||||
user repository.User
|
||||
emailSvc repository.EmailService
|
||||
emailCfg config.Email
|
||||
trxRepo repository.TransactionManager
|
||||
trxRepo repository.Trx
|
||||
license repository.License
|
||||
}
|
||||
|
||||
func New(authRepo repository.Auth,
|
||||
crypto repository.Crypto, user repository.User, emailSvc repository.EmailService,
|
||||
emailCfg config.Email, trxRepo repository.TransactionManager,
|
||||
emailCfg config.Email, trxRepo repository.Trx,
|
||||
license repository.License,
|
||||
) *AuthServiceImpl {
|
||||
return &AuthServiceImpl{
|
||||
|
||||
@ -16,14 +16,14 @@ type Config interface {
|
||||
|
||||
type BalanceService struct {
|
||||
repo repository.WalletRepository
|
||||
trx repository.TransactionManager
|
||||
trx repository.Trx
|
||||
crypt repository.Crypto
|
||||
transaction repository.TransactionRepository
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func NewBalanceService(repo repository.WalletRepository,
|
||||
trx repository.TransactionManager,
|
||||
trx repository.Trx,
|
||||
crypt repository.Crypto, cfg Config,
|
||||
transaction repository.TransactionRepository) *BalanceService {
|
||||
return &BalanceService{
|
||||
|
||||
@ -12,14 +12,14 @@ import (
|
||||
|
||||
type PartnerService struct {
|
||||
repo repository.PartnerRepository
|
||||
trx repository.TransactionManager
|
||||
trx repository.Trx
|
||||
userSvc *users.UserService
|
||||
walletRepo repository.WalletRepository
|
||||
userRepo repository.User
|
||||
}
|
||||
|
||||
func NewPartnerService(repo repository.PartnerRepository,
|
||||
userSvc *users.UserService, repoManager repository.TransactionManager,
|
||||
userSvc *users.UserService, repoManager repository.Trx,
|
||||
walletRepo repository.WalletRepository,
|
||||
userRepo repository.User,
|
||||
) *PartnerService {
|
||||
|
||||
@ -63,7 +63,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
|
||||
productSvcV2, custSvcV2, repo.TransactionRepo,
|
||||
repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings,
|
||||
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)
|
||||
return &ServiceManagerImpl{
|
||||
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 {
|
||||
repo repository.TransactionRepository
|
||||
wallet repository.WalletRepository
|
||||
trx repository.TransactionManager
|
||||
trx repository.Trx
|
||||
}
|
||||
|
||||
func New(repo repository.TransactionRepository,
|
||||
wallet repository.WalletRepository,
|
||||
trx repository.TransactionManager,
|
||||
trx repository.Trx,
|
||||
) *TransactionService {
|
||||
return &TransactionService{
|
||||
repo: repo,
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"enaklo-pos-be/internal/common/logger"
|
||||
"enaklo-pos-be/internal/common/mycontext"
|
||||
"enaklo-pos-be/internal/entity"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -13,6 +14,7 @@ type Service interface {
|
||||
CloseSession(ctx mycontext.Context, sessionID int64, closingAmount float64) (*entity.CashierSessionReport, error)
|
||||
GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, 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 {
|
||||
@ -21,6 +23,7 @@ type Repository interface {
|
||||
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
|
||||
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, 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 {
|
||||
@ -97,3 +100,11 @@ func (s *cashierSessionSvc) GetSessionReport(ctx mycontext.Context, sessionID in
|
||||
Payments: report,
|
||||
}, 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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"enaklo-pos-be/internal/common/logger"
|
||||
"enaklo-pos-be/internal/common/mycontext"
|
||||
order2 "enaklo-pos-be/internal/constants/order"
|
||||
"enaklo-pos-be/internal/entity"
|
||||
"enaklo-pos-be/internal/services/v2/order"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -19,56 +25,185 @@ type InProgressOrderService interface {
|
||||
|
||||
type OrderRepository interface {
|
||||
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)
|
||||
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 {
|
||||
CalculateOrderTotals(
|
||||
ctx mycontext.Context,
|
||||
items []entity.OrderItemRequest,
|
||||
productDetails *entity.ProductDetails,
|
||||
source string,
|
||||
partnerID int64,
|
||||
) (*entity.OrderCalculation, error)
|
||||
CalculateOrderTotals(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)
|
||||
}
|
||||
|
||||
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 {
|
||||
repo OrderRepository
|
||||
orderCalculator OrderCalculator
|
||||
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{
|
||||
repo: repo,
|
||||
orderCalculator: calculator,
|
||||
product: product,
|
||||
trx: trx,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) {
|
||||
productIDs, filteredItems, err := s.orderCalculator.ValidateOrderItems(ctx, req.OrderItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.OrderItems = filteredItems
|
||||
|
||||
productDetails, err := s.product.GetProductDetails(ctx, productIDs, req.PartnerID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderCalculation, err := s.orderCalculator.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source, req.PartnerID)
|
||||
orderItems, err := s.prepareOrderItems(ctx, req.OrderItems, req.PartnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderItems := make([]entity.OrderItem, len(req.OrderItems))
|
||||
for i, item := range req.OrderItems {
|
||||
orderCalculation, err := s.calculateOrderTotals(ctx, req.OrderItems, req.Source, req.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)
|
||||
}
|
||||
}()
|
||||
|
||||
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))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderItems := make([]entity.OrderItem, len(filteredItems))
|
||||
for i, item := range filteredItems {
|
||||
product, exists := productDetails.Products[item.ProductID]
|
||||
productName := ""
|
||||
if exists {
|
||||
@ -76,17 +211,34 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques
|
||||
}
|
||||
|
||||
orderItems[i] = entity.OrderItem{
|
||||
ItemID: item.ProductID,
|
||||
ItemName: productName,
|
||||
Quantity: item.Quantity,
|
||||
Price: product.Price,
|
||||
ItemType: product.Type,
|
||||
Description: product.Description,
|
||||
Notes: item.Notes,
|
||||
ItemID: item.ProductID,
|
||||
ItemName: productName,
|
||||
Quantity: item.Quantity,
|
||||
Price: product.Price,
|
||||
ItemType: product.Type,
|
||||
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,
|
||||
PartnerID: req.PartnerID,
|
||||
CustomerID: req.CustomerID,
|
||||
@ -95,22 +247,50 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques
|
||||
OrderItems: orderItems,
|
||||
TableNumber: req.TableNumber,
|
||||
OrderType: req.OrderType,
|
||||
Total: orderCalculation.Total,
|
||||
Tax: orderCalculation.Tax,
|
||||
Amount: orderCalculation.Subtotal,
|
||||
Total: calculation.Total,
|
||||
Tax: calculation.Tax,
|
||||
Amount: calculation.Subtotal,
|
||||
Status: order2.Pending.String(),
|
||||
Source: req.Source,
|
||||
}
|
||||
}
|
||||
|
||||
createdOrder, err := s.repo.CreateOrUpdate(ctx, order)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to create in-progress order",
|
||||
zap.Error(err),
|
||||
zap.Int64("partnerID", order.PartnerID))
|
||||
return nil, errors.Wrap(err, "failed to create in-progress order")
|
||||
func (s *inProgressOrderSvc) convertToOrderItemRequests(items []entity.OrderItem) []entity.OrderItemRequest {
|
||||
requests := make([]entity.OrderItemRequest, len(items))
|
||||
for i, item := range items {
|
||||
requests[i] = entity.OrderItemRequest{
|
||||
ProductID: item.ItemID,
|
||||
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) {
|
||||
@ -127,79 +307,6 @@ func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partner
|
||||
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) {
|
||||
orders, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||
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/entity"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -108,25 +109,61 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
|
||||
return err
|
||||
}
|
||||
|
||||
// Only allow voiding for NEW, PENDING orders
|
||||
if order.Status != "NEW" && order.Status != "PENDING" {
|
||||
return errors.New("only new or pending orders can be voided")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to void order", zap.Error(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" {
|
||||
// Void specific items
|
||||
voidedAmount := 0.0
|
||||
orderItemMap := make(map[int64]*entity.OrderItem)
|
||||
|
||||
for _, item := range order.OrderItems {
|
||||
orderItemMap[item.ID] = &item
|
||||
for i := range order.OrderItems {
|
||||
orderItemMap[order.OrderItems[i].ID] = &order.OrderItems[i]
|
||||
}
|
||||
|
||||
for _, voidItem := range items {
|
||||
@ -135,55 +172,114 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
|
||||
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 {
|
||||
return errors.New(fmt.Sprintf("void quantity %d exceeds available quantity %d for item %d",
|
||||
voidItem.Quantity, orderItem.Quantity, voidItem.OrderItemID))
|
||||
}
|
||||
|
||||
voidedAmount += orderItem.Price * float64(voidItem.Quantity)
|
||||
}
|
||||
|
||||
// Update order items with reduced quantities
|
||||
for _, voidItem := range items {
|
||||
orderItem := orderItemMap[voidItem.OrderItemID]
|
||||
newQuantity := orderItem.Quantity - voidItem.Quantity
|
||||
|
||||
if newQuantity == 0 {
|
||||
// Remove item completely
|
||||
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, 0)
|
||||
} else {
|
||||
// Update quantity
|
||||
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity)
|
||||
// Create new VOIDED order item with the voided quantity
|
||||
voidedItem := &entity.OrderItem{
|
||||
OrderID: orderID,
|
||||
ItemID: orderItem.ItemID,
|
||||
ItemType: orderItem.ItemType,
|
||||
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 {
|
||||
logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate order totals
|
||||
remainingAmount := order.Amount - voidedAmount
|
||||
remainingTax := (remainingAmount / order.Amount) * order.Tax
|
||||
remainingTotal := remainingAmount + remainingTax
|
||||
|
||||
// Update order totals
|
||||
err = s.repo.UpdateOrderTotals(ctx, orderID, remainingAmount, remainingTax, remainingTotal)
|
||||
updatedOrder, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
|
||||
logger.ContextLogger(ctx).Error("failed to fetch updated order for recalculation", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// Update order status to PARTIAL if some items remain, otherwise to VOIDED
|
||||
newStatus := "PARTIAL"
|
||||
if remainingAmount <= 0 {
|
||||
newStatus = "VOIDED"
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
|
||||
return err
|
||||
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
|
||||
err = s.repo.UpdateOrderTotals(ctx, orderID, orderCalculation.Subtotal, orderCalculation.Tax, orderCalculation.Total)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// Update order status based on remaining amount
|
||||
newStatus := "PENDING"
|
||||
if orderCalculation.Subtotal <= 0 {
|
||||
newStatus = "CANCELED"
|
||||
}
|
||||
|
||||
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,8 +291,7 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
|
||||
return nil
|
||||
}
|
||||
|
||||
// SplitBillRequest handles splitting bills by items or amounts
|
||||
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
|
||||
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
|
||||
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
if splitType == "ITEM" {
|
||||
splitOrder, err = s.splitByItems(ctx, order, paymentMethod, paymentProvider, items)
|
||||
splitOrder, err = s.splitByItems(ctx, order, items)
|
||||
} else if splitType == "AMOUNT" {
|
||||
splitOrder, err = s.splitByAmount(ctx, order, paymentMethod, paymentProvider, amount)
|
||||
splitOrder, err = s.splitByAmount(ctx, order, amount)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -228,12 +323,12 @@ func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID in
|
||||
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
|
||||
orderItemMap := make(map[int64]*entity.OrderItem)
|
||||
|
||||
for _, item := range originalOrder.OrderItems {
|
||||
orderItemMap[item.ID] = &item
|
||||
for i := range originalOrder.OrderItems {
|
||||
orderItemMap[originalOrder.OrderItems[i].ID] = &originalOrder.OrderItems[i]
|
||||
}
|
||||
|
||||
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
|
||||
splitTotal := splitAmount + splitTax
|
||||
|
||||
// Create new PAID order for the split
|
||||
splitOrder := &entity.Order{
|
||||
PartnerID: originalOrder.PartnerID,
|
||||
CustomerID: originalOrder.CustomerID,
|
||||
@ -284,8 +378,6 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
|
||||
Amount: splitAmount,
|
||||
Tax: splitTax,
|
||||
Total: splitTotal,
|
||||
PaymentType: paymentMethod,
|
||||
PaymentProvider: paymentProvider,
|
||||
Source: originalOrder.Source,
|
||||
CreatedBy: originalOrder.CreatedBy,
|
||||
OrderItems: splitOrderItems,
|
||||
@ -300,16 +392,13 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Adjust original order items (reduce quantities)
|
||||
for _, item := range items {
|
||||
orderItem := orderItemMap[item.OrderItemID]
|
||||
newQuantity := orderItem.Quantity - item.Quantity
|
||||
|
||||
if newQuantity == 0 {
|
||||
// Remove item completely
|
||||
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0)
|
||||
} else {
|
||||
// Update quantity
|
||||
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
|
||||
remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax
|
||||
remainingTotal := remainingAmount + remainingTax
|
||||
|
||||
// Update original order totals
|
||||
err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal)
|
||||
if err != nil {
|
||||
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
|
||||
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
|
||||
if amount >= originalOrder.Total {
|
||||
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
|
||||
splitTotal := splitAmount + splitTax
|
||||
|
||||
// Create new PAID order for the split
|
||||
splitOrder := &entity.Order{
|
||||
PartnerID: originalOrder.PartnerID,
|
||||
CustomerID: originalOrder.CustomerID,
|
||||
@ -371,8 +457,6 @@ func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Or
|
||||
Amount: splitAmount,
|
||||
Tax: splitTax,
|
||||
Total: splitTotal,
|
||||
PaymentType: paymentMethod,
|
||||
PaymentProvider: paymentProvider,
|
||||
Source: originalOrder.Source,
|
||||
CreatedBy: originalOrder.CreatedBy,
|
||||
OrderItems: splitOrderItems,
|
||||
|
||||
@ -15,24 +15,12 @@ type Repository interface {
|
||||
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
||||
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) 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)
|
||||
GetOrderPaymentMethodBreakdown(
|
||||
ctx mycontext.Context,
|
||||
partnerID int64,
|
||||
req entity.SearchRequest,
|
||||
) ([]entity.PaymentMethodBreakdown, error)
|
||||
GetRevenueOverview(
|
||||
ctx mycontext.Context,
|
||||
req entity.RevenueOverviewRequest,
|
||||
) ([]entity.RevenueOverviewItem, error)
|
||||
GetSalesByCategory(
|
||||
ctx mycontext.Context,
|
||||
req entity.SalesByCategoryRequest,
|
||||
) ([]entity.SalesByCategoryItem, error)
|
||||
GetPopularProducts(
|
||||
ctx mycontext.Context,
|
||||
req entity.PopularProductsRequest,
|
||||
) ([]entity.PopularProductItem, error)
|
||||
CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error
|
||||
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||
GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error)
|
||||
GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error)
|
||||
GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error)
|
||||
GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, error)
|
||||
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID 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
|
||||
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
|
||||
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error)
|
||||
GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error)
|
||||
GetOrderHistory(ctx mycontext.Context, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||
CalculateOrderTotals(
|
||||
ctx mycontext.Context,
|
||||
items []entity.OrderItemRequest,
|
||||
@ -110,6 +98,7 @@ type Service interface {
|
||||
GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||
GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, 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 {
|
||||
|
||||
@ -4,12 +4,13 @@ import (
|
||||
"enaklo-pos-be/internal/common/logger"
|
||||
"enaklo-pos-be/internal/common/mycontext"
|
||||
"enaklo-pos-be/internal/entity"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) {
|
||||
return s.repo.GetOrderHistoryByPartnerID(ctx, partnerID, request)
|
||||
func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, request entity.SearchRequest) ([]*entity.Order, int64, error) {
|
||||
return s.repo.GetOrderHistoryByPartnerID(ctx, ctx.GetPartnerID(), request)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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