From f31f83e48585ebd848014f6d441029b758f531ad Mon Sep 17 00:00:00 2001 From: "aditya.siregar" Date: Fri, 27 Jun 2025 13:01:39 +0700 Subject: [PATCH] upodate system --- internal/entity/casheer_session.go | 1 + internal/entity/order.go | 36 +- internal/handlers/http/cashier.go | 50 +- internal/handlers/http/customer_order.go | 2 +- internal/handlers/http/order.go | 173 ++-- internal/handlers/request/cashier.go | 2 + internal/handlers/request/product.go | 24 +- internal/handlers/request/query.go | 126 +++ .../request/validator/request_validator.go | 43 + internal/handlers/response/cashier.go | 2 + internal/handlers/response/order.go | 50 + internal/handlers/response/order_inquiry.go | 4 + .../handlers/response/pagination_formatter.go | 34 + .../handlers/response/payment_formatter.go | 20 + internal/repository/In_progress_orde_repo.go | 373 ++++---- internal/repository/casheer_seasion.go | 42 +- internal/repository/models/casheer_seasion.go | 1 + internal/repository/models/order.go | 1 + internal/repository/orde_repo.go | 350 +++---- internal/repository/query_builder.go | 193 ++++ internal/repository/repository.go | 17 +- internal/repository/transaction.go | 32 + internal/routes/routes.go | 3 +- internal/services/auth/init.go | 4 +- internal/services/balance/balance.go | 4 +- internal/services/partner/partner.go | 4 +- internal/services/service.go | 2 +- internal/services/transaction/transaction.go | 4 +- .../v2/cashier_session/casheer_session.go | 11 + .../v2/inprogress_order/in_progress_order.go | 337 ++++--- .../in_progress_order_test.go | 898 ++++++++++++++++++ .../v2/order/advanced_order_management.go | 186 +++- internal/services/v2/order/order.go | 29 +- internal/services/v2/order/order_history.go | 18 +- ...dd_partner_id_to_cashier_sessions.down.sql | 5 + ..._add_partner_id_to_cashier_sessions.up.sql | 8 + 36 files changed, 2400 insertions(+), 689 deletions(-) create mode 100644 internal/handlers/request/query.go create mode 100644 internal/handlers/request/validator/request_validator.go create mode 100644 internal/handlers/response/pagination_formatter.go create mode 100644 internal/handlers/response/payment_formatter.go create mode 100644 internal/repository/query_builder.go create mode 100644 internal/repository/transaction.go create mode 100644 internal/services/v2/inprogress_order/in_progress_order_test.go create mode 100644 migrations/000013_add_partner_id_to_cashier_sessions.down.sql create mode 100644 migrations/000013_add_partner_id_to_cashier_sessions.up.sql diff --git a/internal/entity/casheer_session.go b/internal/entity/casheer_session.go index 75c3b79..e1c4feb 100644 --- a/internal/entity/casheer_session.go +++ b/internal/entity/casheer_session.go @@ -4,6 +4,7 @@ import "time" type CashierSession struct { ID int64 + PartnerID int64 CashierID int64 OpenedAt time.Time ClosedAt *time.Time diff --git a/internal/entity/order.go b/internal/entity/order.go index 69504be..65692b5 100644 --- a/internal/entity/order.go +++ b/internal/entity/order.go @@ -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 { diff --git a/internal/handlers/http/cashier.go b/internal/handlers/http/cashier.go index d251021..2ebdf89 100644 --- a/internal/handlers/http/cashier.go +++ b/internal/handlers/http/cashier.go @@ -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, + }) +} diff --git a/internal/handlers/http/customer_order.go b/internal/handlers/http/customer_order.go index 23b418c..900481f 100644 --- a/internal/handlers/http/customer_order.go +++ b/internal/handlers/http/customer_order.go @@ -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, }) } diff --git a/internal/handlers/http/order.go b/internal/handlers/http/order.go index 7809cfb..2d32e70 100644 --- a/internal/handlers/http/order.go +++ b/internal/handlers/http/order.go @@ -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}), + }) +} diff --git a/internal/handlers/request/cashier.go b/internal/handlers/request/cashier.go index 346113b..9a9e04d 100644 --- a/internal/handlers/request/cashier.go +++ b/internal/handlers/request/cashier.go @@ -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, } diff --git a/internal/handlers/request/product.go b/internal/handlers/request/product.go index 896634c..22a79f1 100644 --- a/internal/handlers/request/product.go +++ b/internal/handlers/request/product.go @@ -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 { diff --git a/internal/handlers/request/query.go b/internal/handlers/request/query.go new file mode 100644 index 0000000..abfcd28 --- /dev/null +++ b/internal/handlers/request/query.go @@ -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") +} diff --git a/internal/handlers/request/validator/request_validator.go b/internal/handlers/request/validator/request_validator.go new file mode 100644 index 0000000..82c41c1 --- /dev/null +++ b/internal/handlers/request/validator/request_validator.go @@ -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 +} diff --git a/internal/handlers/response/cashier.go b/internal/handlers/response/cashier.go index 777932f..575dcd4 100644 --- a/internal/handlers/response/cashier.go +++ b/internal/handlers/response/cashier.go @@ -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, diff --git a/internal/handlers/response/order.go b/internal/handlers/response/order.go index 970cb9d..83cd5ad 100644 --- a/internal/handlers/response/order.go +++ b/internal/handlers/response/order.go @@ -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 +} diff --git a/internal/handlers/response/order_inquiry.go b/internal/handlers/response/order_inquiry.go index 5676a85..2671dad 100644 --- a/internal/handlers/response/order_inquiry.go +++ b/internal/handlers/response/order_inquiry.go @@ -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 diff --git a/internal/handlers/response/pagination_formatter.go b/internal/handlers/response/pagination_formatter.go new file mode 100644 index 0000000..a8be115 --- /dev/null +++ b/internal/handlers/response/pagination_formatter.go @@ -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 +} diff --git a/internal/handlers/response/payment_formatter.go b/internal/handlers/response/payment_formatter.go new file mode 100644 index 0000000..5a3f5f5 --- /dev/null +++ b/internal/handlers/response/payment_formatter.go @@ -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 +} diff --git a/internal/repository/In_progress_orde_repo.go b/internal/repository/In_progress_orde_repo.go index 6ec8aac..bc9485c 100644 --- a/internal/repository/In_progress_orde_repo.go +++ b/internal/repository/In_progress_orde_repo.go @@ -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 +} diff --git a/internal/repository/casheer_seasion.go b/internal/repository/casheer_seasion.go index efceda2..838a6cf 100644 --- a/internal/repository/casheer_seasion.go +++ b/internal/repository/casheer_seasion.go @@ -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 +} diff --git a/internal/repository/models/casheer_seasion.go b/internal/repository/models/casheer_seasion.go index 4a5d78e..fc72625 100644 --- a/internal/repository/models/casheer_seasion.go +++ b/internal/repository/models/casheer_seasion.go @@ -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 diff --git a/internal/repository/models/order.go b/internal/repository/models/order.go index db1eb3f..189efe6 100644 --- a/internal/repository/models/order.go +++ b/internal/repository/models/order.go @@ -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"` diff --git a/internal/repository/orde_repo.go b/internal/repository/orde_repo.go index b2392ef..64de962 100644 --- a/internal/repository/orde_repo.go +++ b/internal/repository/orde_repo.go @@ -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, + } +} diff --git a/internal/repository/query_builder.go b/internal/repository/query_builder.go new file mode 100644 index 0000000..e161c08 --- /dev/null +++ b/internal/repository/query_builder.go @@ -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}} +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 372497f..65bb26b 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -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 +} diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go new file mode 100644 index 0000000..b458b48 --- /dev/null +++ b/internal/repository/transaction.go @@ -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() +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 6493f27..773192a 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -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" diff --git a/internal/services/auth/init.go b/internal/services/auth/init.go index de0f380..d8859a4 100644 --- a/internal/services/auth/init.go +++ b/internal/services/auth/init.go @@ -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{ diff --git a/internal/services/balance/balance.go b/internal/services/balance/balance.go index 6d569fe..3aab24a 100644 --- a/internal/services/balance/balance.go +++ b/internal/services/balance/balance.go @@ -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{ diff --git a/internal/services/partner/partner.go b/internal/services/partner/partner.go index af676b2..e610c4e 100644 --- a/internal/services/partner/partner.go +++ b/internal/services/partner/partner.go @@ -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 { diff --git a/internal/services/service.go b/internal/services/service.go index bf4b8b8..bb8e0ac 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -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), diff --git a/internal/services/transaction/transaction.go b/internal/services/transaction/transaction.go index 6df8daa..9649f61 100644 --- a/internal/services/transaction/transaction.go +++ b/internal/services/transaction/transaction.go @@ -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, diff --git a/internal/services/v2/cashier_session/casheer_session.go b/internal/services/v2/cashier_session/casheer_session.go index 9c1ad00..07d8767 100644 --- a/internal/services/v2/cashier_session/casheer_session.go +++ b/internal/services/v2/cashier_session/casheer_session.go @@ -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 +} diff --git a/internal/services/v2/inprogress_order/in_progress_order.go b/internal/services/v2/inprogress_order/in_progress_order.go index 2ab6c09..c8b75b9 100644 --- a/internal/services/v2/inprogress_order/in_progress_order.go +++ b/internal/services/v2/inprogress_order/in_progress_order.go @@ -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 { diff --git a/internal/services/v2/inprogress_order/in_progress_order_test.go b/internal/services/v2/inprogress_order/in_progress_order_test.go new file mode 100644 index 0000000..7dc09c2 --- /dev/null +++ b/internal/services/v2/inprogress_order/in_progress_order_test.go @@ -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) +} diff --git a/internal/services/v2/order/advanced_order_management.go b/internal/services/v2/order/advanced_order_management.go index eea0443..f3ba9c9 100644 --- a/internal/services/v2/order/advanced_order_management.go +++ b/internal/services/v2/order/advanced_order_management.go @@ -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, diff --git a/internal/services/v2/order/order.go b/internal/services/v2/order/order.go index 4f25098..e98320d 100644 --- a/internal/services/v2/order/order.go +++ b/internal/services/v2/order/order.go @@ -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 { diff --git a/internal/services/v2/order/order_history.go b/internal/services/v2/order/order_history.go index 0a9ac3f..936bce7 100644 --- a/internal/services/v2/order/order_history.go +++ b/internal/services/v2/order/order_history.go @@ -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 +} diff --git a/migrations/000013_add_partner_id_to_cashier_sessions.down.sql b/migrations/000013_add_partner_id_to_cashier_sessions.down.sql new file mode 100644 index 0000000..27b583f --- /dev/null +++ b/migrations/000013_add_partner_id_to_cashier_sessions.down.sql @@ -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; \ No newline at end of file diff --git a/migrations/000013_add_partner_id_to_cashier_sessions.up.sql b/migrations/000013_add_partner_id_to_cashier_sessions.up.sql new file mode 100644 index 0000000..f0de531 --- /dev/null +++ b/migrations/000013_add_partner_id_to_cashier_sessions.up.sql @@ -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); \ No newline at end of file