upodate system

This commit is contained in:
aditya.siregar 2025-06-27 13:01:39 +07:00
parent 1201b2e45b
commit f31f83e485
36 changed files with 2400 additions and 689 deletions

View File

@ -4,6 +4,7 @@ import "time"
type CashierSession struct { type CashierSession struct {
ID int64 ID int64
PartnerID int64
CashierID int64 CashierID int64
OpenedAt time.Time OpenedAt time.Time
ClosedAt *time.Time ClosedAt *time.Time

View File

@ -80,20 +80,20 @@ func (Order) TableName() string {
} }
type OrderItem struct { type OrderItem struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:order_item_id"` ID int64 `gorm:"primaryKey;autoIncrement;column:order_item_id"`
OrderID int64 `gorm:"type:int;column:order_id"` OrderID int64 `gorm:"type:int;column:order_id"`
ItemID int64 `gorm:"type:int;column:item_id"` ItemID int64 `gorm:"type:int;column:item_id"`
ItemType string `gorm:"type:varchar;column:item_type"` ItemType string `gorm:"type:varchar;column:item_type"`
Price float64 `gorm:"type:numeric;not null;column:price"` Price float64 `gorm:"type:numeric;not null;column:price"`
Quantity int `gorm:"type:int;column:quantity"` Quantity int `gorm:"type:int;column:quantity"`
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` Status string `gorm:"type:varchar;column:status;default:ACTIVE"`
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
CreatedBy int64 `gorm:"type:int;column:created_by"` UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
UpdatedBy int64 `gorm:"type:int;column:updated_by"` CreatedBy int64 `gorm:"type:int;column:created_by"`
Product *Product `gorm:"foreignKey:ItemID;references:ID"` UpdatedBy int64 `gorm:"type:int;column:updated_by"`
ItemName string `gorm:"type:varchar;column:item_name"` Product *Product `gorm:"foreignKey:ItemID;references:ID"`
Description string `gorm:"type:varchar;column:description"` ItemName string `gorm:"type:varchar;column:item_name"`
Notes string `gorm:"type:varchar;column:notes"` Notes string `gorm:"type:varchar;column:notes"`
} }
func (OrderItem) TableName() string { func (OrderItem) TableName() string {
@ -135,10 +135,10 @@ type VoidItem struct {
} }
type SplitBillSplit struct { type SplitBillSplit struct {
CustomerName string `json:"customer_name" validate:"required"` CustomerName string `json:"customer_name" validate:"required"`
CustomerID *int64 `json:"customer_id"` CustomerID *int64 `json:"customer_id"`
Items []SplitBillItem `json:"items,omitempty" validate:"required_if=Type ITEM,dive"` Items []SplitBillItem `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"` Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
} }
type SplitBillItem struct { type SplitBillItem struct {

View File

@ -5,9 +5,10 @@ import (
"enaklo-pos-be/internal/handlers/request" "enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/handlers/response" "enaklo-pos-be/internal/handlers/response"
"enaklo-pos-be/internal/services/v2/cashier_session" "enaklo-pos-be/internal/services/v2/cashier_session"
"github.com/gin-gonic/gin"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin"
) )
type CashierSessionHandler struct { type CashierSessionHandler struct {
@ -15,7 +16,9 @@ type CashierSessionHandler struct {
} }
func NewCashierSession(service cashier_session.Service) *CashierSessionHandler { func NewCashierSession(service cashier_session.Service) *CashierSessionHandler {
return &CashierSessionHandler{service: service} return &CashierSessionHandler{
service: service,
}
} }
func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
@ -26,6 +29,7 @@ func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFun
route.POST("/close/:id", h.CloseSession) route.POST("/close/:id", h.CloseSession)
route.GET("/open", h.GetOpenSession) route.GET("/open", h.GetOpenSession)
route.GET("/report/:id", h.GetSessionReport) route.GET("/report/:id", h.GetSessionReport)
route.GET("/history", h.GetSessionHistory)
} }
func (h *CashierSessionHandler) OpenSession(c *gin.Context) { func (h *CashierSessionHandler) OpenSession(c *gin.Context) {
@ -121,3 +125,45 @@ func (h *CashierSessionHandler) GetSessionReport(c *gin.Context) {
Data: response.MapToCashierSessionReport(report), Data: response.MapToCashierSessionReport(report),
}) })
} }
func (h *CashierSessionHandler) GetSessionHistory(c *gin.Context) {
ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
// Parse query parameters
limitStr := c.DefaultQuery("limit", "10")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
sessions, total, err := h.service.GetSessionHistory(ctx, *partnerID, limit, offset)
if err != nil {
response.ErrorWrapper(c, err)
return
}
responseData := make([]*response.CashierSessionResponse, len(sessions))
for i, session := range sessions {
responseData[i] = response.MapToCashierSessionResponse(session)
}
pagingMeta := response.NewPaginationHelper().BuildPagingMeta(offset, limit, total)
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: responseData,
PagingMeta: pagingMeta,
})
}

View File

@ -27,7 +27,6 @@ func (h *CustomerOrderHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc
route.GET("/history", jwt, h.GetOrderHistory) route.GET("/history", jwt, h.GetOrderHistory)
route.GET("/detail/:id", jwt, h.GetOrderID) route.GET("/detail/:id", jwt, h.GetOrderID)
} }
func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) { func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
@ -99,6 +98,7 @@ func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
Price: item.Price, Price: item.Price,
Quantity: item.Quantity, Quantity: item.Quantity,
Subtotal: item.Price * float64(item.Quantity), Subtotal: item.Price * float64(item.Quantity),
Status: item.Status,
}) })
} }

View File

@ -7,20 +7,23 @@ import (
"enaklo-pos-be/internal/handlers/request" "enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/handlers/response" "enaklo-pos-be/internal/handlers/response"
"enaklo-pos-be/internal/services/v2/order" "enaklo-pos-be/internal/services/v2/order"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
type Handler struct { type Handler struct {
service order.Service service order.Service
queryParser *request.QueryParser
} }
func NewOrderHandler(service order.Service) *Handler { func NewOrderHandler(service order.Service) *Handler {
return &Handler{ return &Handler{
service: service, service: service,
queryParser: request.NewQueryParser(),
} }
} }
@ -39,6 +42,7 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route.GET("/revenue-overview", jwt, h.GetRevenueOverview) route.GET("/revenue-overview", jwt, h.GetRevenueOverview)
route.GET("/sales-by-category", jwt, h.GetSalesByCategory) route.GET("/sales-by-category", jwt, h.GetSalesByCategory)
route.GET("/popular-products", jwt, h.GetPopularProducts) route.GET("/popular-products", jwt, h.GetPopularProducts)
route.GET("/detail/:id", jwt, h.GetByID)
} }
type InquiryRequest struct { type InquiryRequest struct {
@ -104,12 +108,10 @@ type VoidItemRequest struct {
} }
type SplitBillRequest struct { type SplitBillRequest struct {
OrderID int64 `json:"order_id" validate:"required"` OrderID int64 `json:"order_id" validate:"required"`
Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"` Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"`
PaymentMethod string `json:"payment_method" validate:"required"` Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
PaymentProvider string `json:"payment_provider"` Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
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 { type SplitBillItemRequest struct {
@ -318,7 +320,7 @@ func (h *Handler) Refund(c *gin.Context) {
Reason: req.Reason, Reason: req.Reason,
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
CustomerName: order.CustomerName, CustomerName: order.CustomerName,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider), PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
} }
c.JSON(http.StatusOK, response.BaseResponse{ c.JSON(http.StatusOK, response.BaseResponse{
@ -330,116 +332,30 @@ func (h *Handler) Refund(c *gin.Context) {
func (h *Handler) GetOrderHistory(c *gin.Context) { func (h *Handler) GetOrderHistory(c *gin.Context) {
ctx := request.GetMyContext(c) ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
limitStr := c.Query("limit") searchReq, err := h.queryParser.ParseSearchRequest(c)
offsetStr := c.Query("offset")
status := c.Query("status")
startDateStr := c.Query("start_date")
endDateStr := c.Query("end_date")
searchReq := entity.SearchRequest{}
if status != "" {
searchReq.Status = status
}
limit := 20
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
if limit > 100 {
limit = 100
}
searchReq.Limit = limit
offset := 0
if offsetStr != "" {
parsedOffset, err := strconv.Atoi(offsetStr)
if err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
searchReq.Offset = offset
if startDateStr != "" {
startDate, err := time.Parse(time.RFC3339, startDateStr)
if err == nil {
searchReq.Start = startDate
}
}
// Parse end date if provided
if endDateStr != "" {
endDate, err := time.Parse(time.RFC3339, endDateStr)
if err == nil {
searchReq.End = endDate
}
}
orders, total, err := h.service.GetOrderHistory(ctx, *partnerID, searchReq)
if err != nil { if err != nil {
response.ErrorWrapper(c, err) response.ErrorWrapper(c, err)
return return
} }
responseData := []response.OrderHistoryResponse{} orders, total, err := h.service.GetOrderHistory(ctx, *searchReq)
for _, order := range orders { if err != nil {
var orderItems []response.OrderItemResponse response.ErrorWrapper(c, err)
for _, item := range order.OrderItems { return
orderItems = append(orderItems, response.OrderItemResponse{
ProductID: item.ItemID,
ProductName: item.ItemName,
Price: item.Price,
Quantity: item.Quantity,
Subtotal: item.Price * float64(item.Quantity),
})
}
responseData = append(responseData, response.OrderHistoryResponse{
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,
})
} }
responseData := response.MapOrderHistoryResponse(orders)
pagingMeta := response.NewPaginationHelper().BuildPagingMeta(searchReq.Offset, searchReq.Limit, total)
c.JSON(http.StatusOK, response.BaseResponse{ c.JSON(http.StatusOK, response.BaseResponse{
Success: true, Success: true,
Status: http.StatusOK, Status: http.StatusOK,
Data: responseData, Data: responseData,
PagingMeta: &response.PagingMeta{ PagingMeta: pagingMeta,
Page: offset + 1,
Total: int64(total),
Limit: limit,
},
}) })
} }
func (h *Handler) formatPayment(payment, provider string) string {
if payment == "CASH" {
return payment
}
return payment + " " + provider
}
func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) { func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) {
ctx := request.GetMyContext(c) ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID() partnerID := ctx.GetPartnerID()
@ -501,7 +417,7 @@ func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) {
paymentBreakdown := make([]PaymentMethodBreakdown, len(paymentAnalysis.PaymentMethodBreakdown)) paymentBreakdown := make([]PaymentMethodBreakdown, len(paymentAnalysis.PaymentMethodBreakdown))
for i, bd := range paymentAnalysis.PaymentMethodBreakdown { for i, bd := range paymentAnalysis.PaymentMethodBreakdown {
paymentBreakdown[i] = PaymentMethodBreakdown{ paymentBreakdown[i] = PaymentMethodBreakdown{
PaymentMethod: h.formatPayment(bd.PaymentType, bd.PaymentProvider), PaymentMethod: response.NewPaymentFormatter().Format(bd.PaymentType, bd.PaymentProvider),
TotalTransactions: bd.TotalTransactions, TotalTransactions: bd.TotalTransactions,
TotalAmount: bd.TotalAmount, TotalAmount: bd.TotalAmount,
} }
@ -632,7 +548,6 @@ func (h *Handler) GetPopularProducts(c *gin.Context) {
func (h *Handler) GetRefundHistory(c *gin.Context) { func (h *Handler) GetRefundHistory(c *gin.Context) {
ctx := request.GetMyContext(c) ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
limitStr := c.Query("limit") limitStr := c.Query("limit")
offsetStr := c.Query("offset") offsetStr := c.Query("offset")
@ -664,8 +579,6 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
} }
searchReq.Offset = offset searchReq.Offset = offset
// Set status to REFUNDED to get only refunded orders
searchReq.Status = "REFUNDED" searchReq.Status = "REFUNDED"
if startDateStr != "" { if startDateStr != "" {
@ -682,7 +595,7 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
} }
} }
orders, total, err := h.service.GetOrderHistory(ctx, *partnerID, searchReq) orders, total, err := h.service.GetOrderHistory(ctx, searchReq)
if err != nil { if err != nil {
response.ErrorWrapper(c, err) response.ErrorWrapper(c, err)
return return
@ -698,7 +611,7 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
Status: order.Status, Status: order.Status,
Amount: order.Amount, Amount: order.Amount,
Total: order.Total, Total: order.Total,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider), PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
TableNumber: order.TableNumber, TableNumber: order.TableNumber,
OrderType: order.OrderType, OrderType: order.OrderType,
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"), CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
@ -759,7 +672,6 @@ func (h *Handler) PartialRefund(c *gin.Context) {
return return
} }
// Calculate refunded amount
refundedAmount := 0.0 refundedAmount := 0.0
var refundedItems []RefundedItemResponse var refundedItems []RefundedItemResponse
@ -789,7 +701,7 @@ func (h *Handler) PartialRefund(c *gin.Context) {
Reason: req.Reason, Reason: req.Reason,
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
CustomerName: order.CustomerName, CustomerName: order.CustomerName,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider), PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
RefundedItems: refundedItems, RefundedItems: refundedItems,
} }
@ -815,7 +727,6 @@ func (h *Handler) VoidOrder(c *gin.Context) {
return return
} }
// Convert request items to entity items
var items []entity.VoidItem var items []entity.VoidItem
if req.Type == "ITEM" { if req.Type == "ITEM" {
items = make([]entity.VoidItem, len(req.Items)) items = make([]entity.VoidItem, len(req.Items))
@ -906,7 +817,8 @@ func (h *Handler) SplitBill(c *gin.Context) {
} }
} }
splitOrder, err := h.service.SplitBillRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Type, req.PaymentMethod, req.PaymentProvider, items, req.Amount) splitOrder, err := h.service.SplitBillRequest(ctx,
*ctx.GetPartnerID(), req.OrderID, req.Type, items, req.Amount)
if err != nil { if err != nil {
response.ErrorWrapper(c, err) response.ErrorWrapper(c, err)
return return
@ -918,3 +830,26 @@ func (h *Handler) SplitBill(c *gin.Context) {
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}), Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}),
}) })
} }
func (h *Handler) GetByID(c *gin.Context) {
ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
orderID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
order, err := h.service.GetOrderByIDAndPartnerID(ctx, orderID, *partnerID)
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: order}),
})
}

View File

@ -3,6 +3,7 @@ package request
import "enaklo-pos-be/internal/entity" import "enaklo-pos-be/internal/entity"
type OpenCashierSessionRequest struct { type OpenCashierSessionRequest struct {
PartnerID int64 `json:"partner_id" validate:"required"`
OpeningAmount float64 `json:"opening_amount" validate:"required,gt=0"` OpeningAmount float64 `json:"opening_amount" validate:"required,gt=0"`
} }
@ -12,6 +13,7 @@ type CloseCashierSessionRequest struct {
func (o *OpenCashierSessionRequest) ToEntity(cashierID int64) *entity.CashierSession { func (o *OpenCashierSessionRequest) ToEntity(cashierID int64) *entity.CashierSession {
return &entity.CashierSession{ return &entity.CashierSession{
PartnerID: o.PartnerID,
CashierID: cashierID, CashierID: cashierID,
OpeningAmount: o.OpeningAmount, OpeningAmount: o.OpeningAmount,
} }

View File

@ -30,19 +30,17 @@ func (p *ProductParam) ToEntity(partnerID int64) entity.ProductSearch {
} }
type Product struct { type Product struct {
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
PartnerID int64 `json:"partner_id"` PartnerID int64 `json:"partner_id"`
SiteID int64 `json:"site_id"` SiteID int64 `json:"site_id"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Type string `json:"type"` Type string `json:"type"`
Price float64 `json:"price" validate:"required"` Price float64 `json:"price" validate:"required"`
IsWeekendTicket bool `json:"is_weekend_ticket"` Status string `json:"status"`
IsSeasonTicket bool `json:"is_season_ticket"` Description string `json:"description"`
Status string `json:"status"` Stock int64 `json:"stock"`
Description string `json:"description"` Image string `json:"image"`
Stock int64 `json:"stock"` CategoryID int64 `json:"category_id"`
Image string `json:"image"`
CategoryID int64 `json:"category_id"`
} }
func (e *Product) ToEntity() *entity.Product { func (e *Product) ToEntity() *entity.Product {

View File

@ -0,0 +1,126 @@
package request
import (
"enaklo-pos-be/internal/entity"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"strconv"
"time"
)
type QueryParser struct {
defaultLimit int
maxLimit int
}
func NewQueryParser() *QueryParser {
return &QueryParser{
defaultLimit: 20,
maxLimit: 100,
}
}
func (p *QueryParser) ParseSearchRequest(c *gin.Context) (*entity.SearchRequest, error) {
req := &entity.SearchRequest{}
if status := c.Query("status"); status != "" {
req.Status = status
}
limit, err := p.parseLimit(c.Query("limit"))
if err != nil {
return nil, errors.Wrap(err, "invalid limit parameter")
}
req.Limit = limit
offset, err := p.parseOffset(c.Query("offset"))
if err != nil {
return nil, errors.Wrap(err, "invalid offset parameter")
}
req.Offset = offset
if err := p.parseDateRange(c, req); err != nil {
return nil, errors.Wrap(err, "invalid date parameters")
}
return req, nil
}
func (p *QueryParser) parseLimit(limitStr string) (int, error) {
if limitStr == "" {
return p.defaultLimit, nil
}
limit, err := strconv.Atoi(limitStr)
if err != nil {
return 0, errors.New("limit must be a valid integer")
}
if limit <= 0 {
return p.defaultLimit, nil
}
if limit > p.maxLimit {
limit = p.maxLimit
}
return limit, nil
}
func (p *QueryParser) parseOffset(offsetStr string) (int, error) {
if offsetStr == "" {
return 0, nil
}
offset, err := strconv.Atoi(offsetStr)
if err != nil {
return 0, errors.New("offset must be a valid integer")
}
if offset < 0 {
return 0, nil
}
return offset, nil
}
func (p *QueryParser) parseDateRange(c *gin.Context, req *entity.SearchRequest) error {
if startDateStr := c.Query("start_date"); startDateStr != "" {
startDate, err := p.parseDate(startDateStr)
if err != nil {
return errors.Wrap(err, "invalid start_date format")
}
req.Start = startDate
}
if endDateStr := c.Query("end_date"); endDateStr != "" {
endDate, err := p.parseDate(endDateStr)
if err != nil {
return errors.Wrap(err, "invalid end_date format")
}
req.End = endDate
}
if !req.Start.IsZero() && !req.End.IsZero() && req.Start.After(req.End) {
return errors.New("start_date cannot be after end_date")
}
return nil
}
func (p *QueryParser) parseDate(dateStr string) (time.Time, error) {
formats := []string{
time.RFC3339,
"2006-01-02T15:04:05",
"2006-01-02",
"2006-01-02 15:04:05",
}
for _, format := range formats {
if date, err := time.Parse(format, dateStr); err == nil {
return date, nil
}
}
return time.Time{}, errors.New("unsupported date format")
}

View File

@ -0,0 +1,43 @@
package validator
import (
"enaklo-pos-be/internal/entity"
"github.com/pkg/errors"
"time"
)
type RequestValidator struct{}
func NewRequestValidator() *RequestValidator {
return &RequestValidator{}
}
func (v *RequestValidator) ValidateSearchRequest(req *entity.SearchRequest) error {
if req.Status != "" {
validStatuses := []string{"pending", "confirmed", "processing", "completed", "cancelled"}
if !v.isValidStatus(req.Status, validStatuses) {
return errors.New("invalid status value")
}
}
if !req.Start.IsZero() && !req.End.IsZero() {
if req.Start.After(req.End) {
return errors.New("start date cannot be after end date")
}
if req.End.Sub(req.Start) > 365*24*time.Hour {
return errors.New("date range cannot exceed 1 year")
}
}
return nil
}
func (v *RequestValidator) isValidStatus(status string, validStatuses []string) bool {
for _, validStatus := range validStatuses {
if status == validStatus {
return true
}
}
return false
}

View File

@ -7,6 +7,7 @@ import (
type CashierSessionResponse struct { type CashierSessionResponse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
PartnerID int64 `json:"partner_id"`
CashierID int64 `json:"cashier_id"` CashierID int64 `json:"cashier_id"`
OpenedAt time.Time `json:"opened_at"` OpenedAt time.Time `json:"opened_at"`
ClosedAt *time.Time `json:"closed_at,omitempty"` ClosedAt *time.Time `json:"closed_at,omitempty"`
@ -35,6 +36,7 @@ func MapToCashierSessionResponse(e *entity.CashierSession) *CashierSessionRespon
return &CashierSessionResponse{ return &CashierSessionResponse{
ID: e.ID, ID: e.ID,
PartnerID: e.PartnerID,
CashierID: e.CashierID, CashierID: e.CashierID,
OpenedAt: e.OpenedAt, OpenedAt: e.OpenedAt,
ClosedAt: e.ClosedAt, ClosedAt: e.ClosedAt,

View File

@ -3,6 +3,7 @@ package response
import ( import (
"enaklo-pos-be/internal/constants/order" "enaklo-pos-be/internal/constants/order"
"enaklo-pos-be/internal/constants/transaction" "enaklo-pos-be/internal/constants/transaction"
"enaklo-pos-be/internal/entity"
"time" "time"
) )
@ -204,3 +205,52 @@ type OrderHistoryResponse struct {
Tax float64 `json:"tax"` Tax float64 `json:"tax"`
RestaurantName string `json:"restaurant_name"` RestaurantName string `json:"restaurant_name"`
} }
func MapOrderHistoryResponse(orders []*entity.Order) []OrderHistoryResponse {
responseData := make([]OrderHistoryResponse, 0, len(orders))
for _, order := range orders {
orderResponse := mapOrderToResponse(order)
responseData = append(responseData, orderResponse)
}
return responseData
}
func mapOrderToResponse(order *entity.Order) OrderHistoryResponse {
paymentFormatter := NewPaymentFormatter()
return OrderHistoryResponse{
ID: order.ID,
CustomerName: order.CustomerName,
CustomerID: order.CustomerID,
IsMember: order.IsMemberOrder(),
Status: order.Status,
Amount: order.Amount,
Total: order.Total,
PaymentType: paymentFormatter.Format(order.PaymentType, order.PaymentProvider),
TableNumber: order.TableNumber,
OrderType: order.OrderType,
OrderItems: mapOrderItems(order.OrderItems),
CreatedAt: order.CreatedAt.Format(time.RFC3339),
Tax: order.Tax,
}
}
func mapOrderItems(items []entity.OrderItem) []OrderItemResponse {
orderItems := make([]OrderItemResponse, 0, len(items))
for _, item := range items {
orderItems = append(orderItems, OrderItemResponse{
OrderItemID: item.ID,
ProductID: item.ItemID,
ProductName: item.ItemName,
Price: item.Price,
Quantity: item.Quantity,
Subtotal: item.Price * float64(item.Quantity),
Notes: item.Notes,
Status: item.Status,
})
}
return orderItems
}

View File

@ -23,12 +23,14 @@ type OrderInquiryResponse struct {
} }
type OrderItemResponse struct { type OrderItemResponse struct {
OrderItemID int64 `json:"order_item_id"`
ProductID int64 `json:"product_id"` ProductID int64 `json:"product_id"`
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
Price float64 `json:"price"` Price float64 `json:"price"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
Subtotal float64 `json:"subtotal"` Subtotal float64 `json:"subtotal"`
Notes string `json:"notes"` Notes string `json:"notes"`
Status string `json:"status"`
} }
func mapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse { func mapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
@ -109,12 +111,14 @@ func MapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
result := make([]OrderItemResponse, 0, len(items)) result := make([]OrderItemResponse, 0, len(items))
for _, item := range items { for _, item := range items {
result = append(result, OrderItemResponse{ result = append(result, OrderItemResponse{
OrderItemID: item.ID,
ProductID: item.ItemID, ProductID: item.ItemID,
ProductName: item.ItemName, ProductName: item.ItemName,
Price: item.Price, Price: item.Price,
Quantity: item.Quantity, Quantity: item.Quantity,
Subtotal: item.Price * float64(item.Quantity), Subtotal: item.Price * float64(item.Quantity),
Notes: item.Notes, Notes: item.Notes,
Status: item.Status,
}) })
} }
return result return result

View File

@ -0,0 +1,34 @@
package response
type PaginationHelper struct{}
func NewPaginationHelper() *PaginationHelper {
return &PaginationHelper{}
}
func (p *PaginationHelper) BuildPagingMeta(offset, limit int, total int64) *PagingMeta {
page := 1
if limit > 0 {
page = (offset / limit) + 1
}
return &PagingMeta{
Page: page,
Total: total,
Limit: limit,
}
}
func (p *PaginationHelper) calculateTotalPages(total int64, limit int) int {
if limit <= 0 {
return 1
}
return int((total + int64(limit) - 1) / int64(limit))
}
func (p *PaginationHelper) hasNextPage(offset, limit int, total int64) bool {
if limit <= 0 {
return false
}
return int64(offset+limit) < total
}

View File

@ -0,0 +1,20 @@
package response
import "fmt"
type PaymentFormatter interface {
Format(paymentType, paymentProvider string) string
}
type paymentFormatter struct{}
func NewPaymentFormatter() PaymentFormatter {
return &paymentFormatter{}
}
func (f *paymentFormatter) Format(paymentType, paymentProvider string) string {
if paymentProvider != "" {
return fmt.Sprintf("%s (%s)", paymentType, paymentProvider)
}
return paymentType
}

View File

@ -1,112 +1,116 @@
package repository package repository
import ( import (
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository/models" "enaklo-pos-be/internal/repository/models"
"enaklo-pos-be/internal/services/v2/inprogress_order"
time2 "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"gorm.io/gorm" "gorm.io/gorm"
time2 "time"
) )
type InProgressOrderRepository interface { type InProgressOrderRepository interface {
CreateOrUpdate(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error)
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error)
CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
} }
type inprogressOrderRepository struct { type inprogressOrderRepository struct {
db *gorm.DB db *gorm.DB
} }
func NewInProgressOrderRepository(db *gorm.DB) *inprogressOrderRepository { func NewInProgressOrderRepository(db *gorm.DB) inprogress_order.OrderRepository {
return &inprogressOrderRepository{db: db} return &inprogressOrderRepository{db: db}
} }
func (r *inprogressOrderRepository) CreateOrUpdate(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) { func (r *inprogressOrderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) {
isUpdate := order.ID != "" var orderDB models.OrderDB
tx := r.db.Begin() if err := r.db.Preload("OrderItems").First(&orderDB, id).Error; err != nil {
if tx.Error != nil { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.Wrap(tx.Error, "failed to begin transaction") return nil, errors.New("order not found")
}
return nil, errors.Wrap(err, "failed to find order")
} }
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
orderDB := r.toInProgressOrderDBModel(order) order := r.toDomainOrderModel(&orderDB)
if isUpdate { return order, nil
var existingOrder models.InProgressOrderDB }
if err := tx.First(&existingOrder, order.ID).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "order not found for update")
}
if err := tx.Model(&orderDB).Updates(orderDB).Error; err != nil { func (r *inprogressOrderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
tx.Rollback() orderDB := r.toOrderDBModel(order)
return nil, errors.Wrap(err, "failed to update order")
}
if err := tx.Where("in_progress_order_id = ?", order.ID).Delete(&models.InProgressOrderItemDB{}).Error; err != nil { // Use provided transaction or create new one
tx.Rollback() var dbTx *gorm.DB
return nil, errors.Wrap(err, "failed to delete existing order items") if tx != nil {
} dbTx = tx
} else { } else {
if err := tx.Create(&orderDB).Error; err != nil { dbTx = r.db.Begin()
tx.Rollback() if dbTx.Error != nil {
return nil, errors.Wrap(err, "failed to insert order") return nil, errors.Wrap(dbTx.Error, "failed to begin transaction")
} }
defer func() {
order.ID = orderDB.ID if r := recover(); r != nil {
dbTx.Rollback()
}
}()
} }
var itemIDs []int64 if err := dbTx.Create(&orderDB).Error; err != nil {
for i := range order.OrderItems { if tx == nil {
itemIDs = append(itemIDs, order.OrderItems[i].ItemID) dbTx.Rollback()
}
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")
} }
return nil, errors.Wrap(err, "failed to insert order")
} }
order.ID = orderDB.ID
productMap := make(map[int64]models.ProductDB) // Only commit if we created the transaction
for _, product := range products { if tx == nil {
productMap[product.ID] = product if err := dbTx.Commit().Error; err != nil {
} return nil, errors.Wrap(err, "failed to commit transaction")
for i := range order.OrderItems {
item := &order.OrderItems[i]
itemDB := r.toOrderItemDBModel(item, orderDB.ID)
if err := tx.Create(&itemDB).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to insert order item")
} }
item.ID = itemDB.ID
if product, exists := productMap[item.ItemID]; exists {
item.Product = r.toDomainProductModel(&product)
}
}
if err := tx.Commit().Error; err != nil {
return nil, errors.Wrap(err, "failed to commit transaction")
} }
return order, nil return order, nil
} }
func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) { func (r *inprogressOrderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
var ordersDB []models.InProgressOrderDB if len(items) == 0 {
query := r.db.Where("partner_id = ?", partnerID).Order("created_at DESC") return nil
}
itemsDB := make([]models.OrderItemDB, len(items))
for i, item := range items {
itemDB := r.toOrderItemDBModel(&item)
itemDB.OrderID = orderID
itemsDB[i] = itemDB
}
if err := tx.Create(&itemsDB).Error; err != nil {
return errors.Wrap(err, "failed to bulk insert order items")
}
for i := range items {
items[i].ID = itemsDB[i].ID
}
return nil
}
func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) {
var ordersDB []models.OrderDB
query := r.db.Where("partner_id = ?", partnerID)
if status != "" {
query = query.Where("status = ?", status)
}
query = query.Order("created_at DESC")
if limit > 0 { if limit > 0 {
query = query.Limit(limit) query = query.Limit(limit)
@ -116,27 +120,39 @@ func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, pa
query = query.Offset(offset) query = query.Offset(offset)
} }
if err := query.Preload("OrderItems.Product").Find(&ordersDB).Error; err != nil { if err := query.Find(&ordersDB).Error; err != nil {
return nil, errors.Wrap(err, "failed to find orders by partner ID") return nil, errors.Wrap(err, "failed to find orders by partner ID")
} }
orders := make([]*entity.InProgressOrder, 0, len(ordersDB)) orders := make([]*entity.Order, 0, len(ordersDB))
for _, orderDB := range ordersDB { for _, orderDB := range ordersDB {
order := r.toDomainOrderModel(&orderDB) order := r.toDomainOrderModel(&orderDB)
order.OrderItems = make([]entity.InProgressOrderItem, 0, len(orderDB.OrderItems))
for _, itemDB := range orderDB.OrderItems { var orderItems []models.OrderItemDB
if err := r.db.Where("order_id = ?", orderDB.ID).Find(&orderItems).Error; err != nil {
return nil, errors.Wrap(err, "failed to find order items")
}
order.OrderItems = make([]entity.OrderItem, 0, len(orderItems))
for _, itemDB := range orderItems {
item := r.toDomainOrderItemModel(&itemDB) item := r.toDomainOrderItemModel(&itemDB)
orderItem := entity.InProgressOrderItem{ orderItem := entity.OrderItem{
ID: item.ID, ID: item.ID,
ItemID: item.ItemID, ItemID: item.ItemID,
Quantity: item.Quantity, Quantity: item.Quantity,
ItemName: item.ItemName,
} }
if itemDB.Product.ID > 0 { if itemDB.ItemID > 0 {
productDomain := r.toDomainProductModel(&itemDB.Product) var product models.ProductDB
orderItem.Product = productDomain err := r.db.First(&product, itemDB.ItemID).Error
if err == nil {
productDomain := r.toDomainProductModel(&product)
orderItem.Product = productDomain
}
} }
order.OrderItems = append(order.OrderItems, orderItem) order.OrderItems = append(order.OrderItems, orderItem)
@ -148,106 +164,122 @@ func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, pa
return orders, nil return orders, nil
} }
func (r *inprogressOrderRepository) toInProgressOrderDBModel(order *entity.InProgressOrder) models.InProgressOrderDB { func (r *inprogressOrderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) {
var orderDB models.OrderDB
if err := r.db.Preload("OrderItems").Where("id = ? AND partner_id = ?", id, partnerID).First(&orderDB).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("order not found")
}
return nil, errors.Wrap(err, "failed to find order")
}
order := r.toDomainOrderModel(&orderDB)
for _, itemDB := range orderDB.OrderItems {
item := r.toDomainOrderItemModel(&itemDB)
order.OrderItems = append(order.OrderItems, *item)
}
return order, nil
}
func (r *inprogressOrderRepository) toOrderDBModel(order *entity.Order) models.OrderDB {
now := time2.Now() now := time2.Now()
return models.InProgressOrderDB{ return models.OrderDB{
ID: order.ID, ID: order.ID,
PartnerID: order.PartnerID, PartnerID: order.PartnerID,
CustomerID: order.CustomerID, CustomerID: order.CustomerID,
CustomerName: order.CustomerName, CustomerName: order.CustomerName,
PaymentType: order.PaymentType, PaymentType: order.PaymentType,
CreatedBy: order.CreatedBy, PaymentProvider: order.PaymentProvider,
CreatedAt: now, CreatedBy: order.CreatedBy,
UpdatedAt: now, CreatedAt: now,
TableNumber: order.TableNumber, UpdatedAt: now,
OrderType: order.OrderType, 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 { func (r *inprogressOrderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order {
return &entity.InProgressOrder{ orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems))
ID: dbModel.ID, for _, itemDB := range dbModel.OrderItems {
PartnerID: dbModel.PartnerID, orderItems = append(orderItems, entity.OrderItem{
CustomerID: dbModel.CustomerID, ItemID: itemDB.ItemID,
CustomerName: dbModel.CustomerName, ItemType: itemDB.ItemType,
PaymentType: dbModel.PaymentType, ItemName: itemDB.ItemName,
CreatedBy: dbModel.CreatedBy, Price: itemDB.Price,
OrderItems: []entity.InProgressOrderItem{}, Quantity: itemDB.Quantity,
TableNumber: dbModel.TableNumber, Status: itemDB.Status,
OrderType: dbModel.OrderType, CreatedBy: itemDB.CreatedBy,
CreatedAt: dbModel.CreatedAt, CreatedAt: itemDB.CreatedAt,
UpdatedAt: dbModel.UpdatedAt, 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 { func (r *inprogressOrderRepository) toOrderItemDBModel(item *entity.OrderItem) models.OrderItemDB {
return models.InProgressOrderItemDB{ return models.OrderItemDB{
ID: item.ID, ID: item.ID,
InProgressOrderIO: inprogressOrderID, OrderID: item.OrderID,
ItemID: item.ItemID, ItemID: item.ItemID,
Quantity: item.Quantity, 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{ return &entity.OrderItem{
ID: dbModel.ID, ID: dbModel.ID,
OrderID: dbModel.OrderID,
ItemID: dbModel.ItemID, ItemID: dbModel.ItemID,
ItemType: dbModel.ItemType,
Price: dbModel.Price,
Quantity: dbModel.Quantity, Quantity: dbModel.Quantity,
Status: dbModel.Status,
CreatedBy: dbModel.CreatedBy, CreatedBy: dbModel.CreatedBy,
CreatedAt: dbModel.CreatedAt, CreatedAt: dbModel.CreatedAt,
ItemName: dbModel.ItemName,
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 { func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product {
if productDB == nil { if productDB == nil {
return nil return nil
@ -264,3 +296,26 @@ func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.Produ
Image: productDB.Image, Image: productDB.Image,
} }
} }
func (r *inprogressOrderRepository) UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error {
now := time2.Now()
result := trx.Model(&models.OrderDB{}).
Where("id = ?", orderID).
Updates(map[string]interface{}{
"amount": amount,
"tax": tax,
"total": total,
"updated_at": now,
})
if result.Error != nil {
return errors.Wrap(result.Error, "failed to update order totals")
}
if result.RowsAffected == 0 {
logger.ContextLogger(ctx).Warn("no order updated")
}
return nil
}

View File

@ -4,9 +4,10 @@ import (
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository/models" "enaklo-pos-be/internal/repository/models"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type CashierSessionRepository interface { type CashierSessionRepository interface {
@ -15,6 +16,7 @@ type CashierSessionRepository interface {
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error) GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error)
GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error) GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error)
GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error)
} }
type cashierSessionRepository struct { type cashierSessionRepository struct {
@ -27,6 +29,7 @@ func NewCashierSessionRepository(db *gorm.DB) CashierSessionRepository {
func (r *cashierSessionRepository) CreateSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) { func (r *cashierSessionRepository) CreateSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) {
dbModel := models.CashierSessionDB{ dbModel := models.CashierSessionDB{
PartnerID: session.PartnerID,
CashierID: session.CashierID, CashierID: session.CashierID,
OpenedAt: time.Now(), OpenedAt: time.Now(),
OpeningAmount: session.OpeningAmount, OpeningAmount: session.OpeningAmount,
@ -94,6 +97,7 @@ func (r *cashierSessionRepository) GetSessionByID(ctx mycontext.Context, session
func (r *cashierSessionRepository) toEntity(db *models.CashierSessionDB) *entity.CashierSession { func (r *cashierSessionRepository) toEntity(db *models.CashierSessionDB) *entity.CashierSession {
return &entity.CashierSession{ return &entity.CashierSession{
ID: db.ID, ID: db.ID,
PartnerID: db.PartnerID,
CashierID: db.CashierID, CashierID: db.CashierID,
OpenedAt: db.OpenedAt, OpenedAt: db.OpenedAt,
ClosedAt: db.ClosedAt, ClosedAt: db.ClosedAt,
@ -135,3 +139,39 @@ func (r *cashierSessionRepository) GetPaymentSummaryBySessionID(ctx mycontext.Co
return summary, nil return summary, nil
} }
func (r *cashierSessionRepository) GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) {
var sessionsDB []models.CashierSessionDB
var totalCount int64
// Count total records
if err := r.db.Model(&models.CashierSessionDB{}).
Where("partner_id = ?", partnerID).
Count(&totalCount).Error; err != nil {
return nil, 0, errors.Wrap(err, "failed to count cashier sessions")
}
// Get sessions with pagination
query := r.db.Where("partner_id = ?", partnerID).
Order("opened_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Find(&sessionsDB).Error; err != nil {
return nil, 0, errors.Wrap(err, "failed to get cashier session history")
}
// Convert to entity
sessions := make([]*entity.CashierSession, len(sessionsDB))
for i, sessionDB := range sessionsDB {
sessions[i] = r.toEntity(&sessionDB)
}
return sessions, totalCount, nil
}

View File

@ -4,6 +4,7 @@ import "time"
type CashierSessionDB struct { type CashierSessionDB struct {
ID int64 `gorm:"primaryKey"` ID int64 `gorm:"primaryKey"`
PartnerID int64 `gorm:"not null"`
CashierID int64 `gorm:"not null"` CashierID int64 `gorm:"not null"`
OpenedAt time.Time `gorm:"not null"` OpenedAt time.Time `gorm:"not null"`
ClosedAt *time.Time ClosedAt *time.Time

View File

@ -39,6 +39,7 @@ type OrderItemDB struct {
ItemType string `gorm:"column:item_type"` ItemType string `gorm:"column:item_type"`
Price float64 `gorm:"column:price"` Price float64 `gorm:"column:price"`
Quantity int `gorm:"column:quantity"` Quantity int `gorm:"column:quantity"`
Status string `gorm:"column:status;default:ACTIVE"`
CreatedBy int64 `gorm:"column:created_by"` CreatedBy int64 `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"column:created_at"` CreatedAt time.Time `gorm:"column:created_at"`
Product ProductDB `gorm:"foreignKey:ItemID;references:ID"` Product ProductDB `gorm:"foreignKey:ItemID;references:ID"`

View File

@ -5,10 +5,11 @@ import (
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository/models" "enaklo-pos-be/internal/repository/models"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type OrderRepository interface { type OrderRepository interface {
@ -17,32 +18,22 @@ type OrderRepository interface {
CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error)
FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error)
UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error)
CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error
CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
GetOrderPaymentMethodBreakdown( GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error)
ctx mycontext.Context, GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error)
partnerID int64, GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error)
req entity.SearchRequest, GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, error)
) ([]entity.PaymentMethodBreakdown, error)
GetRevenueOverview(
ctx mycontext.Context,
req entity.RevenueOverviewRequest,
) ([]entity.RevenueOverviewItem, error)
GetSalesByCategory(
ctx mycontext.Context,
req entity.SalesByCategoryRequest,
) ([]entity.SalesByCategoryItem, error)
GetPopularProducts(
ctx mycontext.Context,
req entity.PopularProductsRequest,
) ([]entity.PopularProductItem, error)
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error) FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error)
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error
} }
type orderRepository struct { type orderRepository struct {
@ -129,11 +120,6 @@ func (r *orderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Ord
order := r.toDomainOrderModel(&orderDB) order := r.toDomainOrderModel(&orderDB)
for _, itemDB := range orderDB.OrderItems {
item := r.toDomainOrderItemModel(&itemDB)
order.OrderItems = append(order.OrderItems, *item)
}
return order, nil return order, nil
} }
@ -281,15 +267,16 @@ func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB {
} }
func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order { func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order {
orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems)) orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems))
for _, itemDB := range dbModel.OrderItems { for _, itemDB := range dbModel.OrderItems {
orderItems = append(orderItems, entity.OrderItem{ orderItems = append(orderItems, entity.OrderItem{
ID: itemDB.ID,
ItemID: itemDB.ItemID, ItemID: itemDB.ItemID,
ItemType: itemDB.ItemType, ItemType: itemDB.ItemType,
ItemName: itemDB.ItemName, ItemName: itemDB.ItemName,
Price: itemDB.Price, Price: itemDB.Price,
Quantity: itemDB.Quantity, Quantity: itemDB.Quantity,
Status: itemDB.Status,
CreatedBy: itemDB.CreatedBy, CreatedBy: itemDB.CreatedBy,
CreatedAt: itemDB.CreatedAt, CreatedAt: itemDB.CreatedAt,
Notes: itemDB.Notes, Notes: itemDB.Notes,
@ -327,6 +314,7 @@ func (r *orderRepository) toOrderItemDBModel(item *entity.OrderItem) models.Orde
ItemName: item.ItemName, ItemName: item.ItemName,
Price: item.Price, Price: item.Price,
Quantity: item.Quantity, Quantity: item.Quantity,
Status: item.Status,
CreatedBy: item.CreatedBy, CreatedBy: item.CreatedBy,
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
Notes: item.Notes, Notes: item.Notes,
@ -341,9 +329,11 @@ func (r *orderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *e
ItemType: dbModel.ItemType, ItemType: dbModel.ItemType,
Price: dbModel.Price, Price: dbModel.Price,
Quantity: dbModel.Quantity, Quantity: dbModel.Quantity,
Status: dbModel.Status,
CreatedBy: dbModel.CreatedBy, CreatedBy: dbModel.CreatedBy,
CreatedAt: dbModel.CreatedAt, CreatedAt: dbModel.CreatedAt,
ItemName: dbModel.ItemName, ItemName: dbModel.ItemName,
Notes: dbModel.Notes,
Product: &entity.Product{ Product: &entity.Product{
ID: dbModel.ItemID, ID: dbModel.ItemID,
Name: dbModel.ItemName, Name: dbModel.ItemName,
@ -406,65 +396,70 @@ func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiry
return inquiry return inquiry
} }
func (r *orderRepository) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) { func (r *orderRepository) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error) {
var ordersDB []models.OrderDB queryBuilder := NewQueryBuilder[models.OrderDB](r.db)
var totalCount int64 filters := []Filter{
Equal("partner_id", partnerID),
}
// Build the base query
baseQuery := r.db.Model(&models.OrderDB{}).Where("partner_id = ?", partnerID)
// Apply filters to the base query
if req.Status != "" { if req.Status != "" {
baseQuery = baseQuery.Where("status = ?", req.Status) filters = append(filters, Equal("status", req.Status))
} }
if !req.Start.IsZero() { if !req.Start.IsZero() {
baseQuery = baseQuery.Where("created_at >= ?", req.Start) filters = append(filters, GreaterEqual("created_at", req.Start))
} }
if !req.End.IsZero() { if !req.End.IsZero() {
baseQuery = baseQuery.Where("created_at <= ?", req.End) filters = append(filters, LessEqual("created_at", req.End))
} }
// Get total count with the current filters before pagination options := QueryOptions{
if err := baseQuery.Count(&totalCount).Error; err != nil { Filters: filters,
return nil, 0, errors.Wrap(err, "failed to count total orders") Limit: req.Limit,
Offset: req.Offset,
OrderBy: []string{"created_at DESC"},
Preloads: []string{"OrderItems"},
} }
// Clone the query for fetching the actual data with pagination baseQuery := queryBuilder.BuildQuery(options)
query := baseQuery.Session(&gorm.Session{}) totalCount, err := queryBuilder.Count(baseQuery)
if err != nil {
// Add ordering and pagination return nil, 0, err
query = query.Order("created_at DESC")
if req.Limit > 0 {
query = query.Limit(req.Limit)
} }
if req.Offset > 0 { query := queryBuilder.ExecuteQuery(baseQuery, options)
query = query.Offset(req.Offset) ordersDB, err := queryBuilder.Find(query)
if err != nil {
return nil, 0, err
} }
// Execute the query with preloading orders := r.convertOrdersToEntity(ordersDB)
if err := query.Preload("OrderItems").Find(&ordersDB).Error; err != nil {
return nil, 0, errors.Wrap(err, "failed to find order history by partner ID")
}
// Map to domain models return orders, totalCount, nil
}
func (r *orderRepository) convertOrdersToEntity(ordersDB []models.OrderDB) []*entity.Order {
orders := make([]*entity.Order, 0, len(ordersDB)) orders := make([]*entity.Order, 0, len(ordersDB))
for _, orderDB := range ordersDB { for _, orderDB := range ordersDB {
order := r.toDomainOrderModel(&orderDB) order := r.toDomainOrderModel(&orderDB)
order.OrderItems = make([]entity.OrderItem, 0, len(orderDB.OrderItems)) order.OrderItems = r.convertOrderItemsToEntity(orderDB.OrderItems)
for _, itemDB := range orderDB.OrderItems {
item := r.toDomainOrderItemModel(&itemDB)
order.OrderItems = append(order.OrderItems, *item)
}
orders = append(orders, order) orders = append(orders, order)
} }
return orders, totalCount, nil return orders
}
func (r *orderRepository) convertOrderItemsToEntity(itemsDB []models.OrderItemDB) []entity.OrderItem {
items := make([]entity.OrderItem, 0, len(itemsDB))
for _, itemDB := range itemsDB {
item := r.toDomainOrderItemModel(&itemDB)
items = append(items, *item)
}
return items
} }
func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) { func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) {
@ -521,126 +516,108 @@ func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID
return orders, totalCount, nil return orders, totalCount, nil
} }
func (r *orderRepository) CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) { func (r *orderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
isUpdate := order.ID != 0 orderDB := r.toOrderDBModel(order)
tx := r.db.Begin() // Use provided transaction or create new one
if tx.Error != nil { var dbTx *gorm.DB
return nil, errors.Wrap(tx.Error, "failed to begin transaction") if tx != nil {
} dbTx = tx
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
orderDB := r.toInProgressOrderDBModel(order)
if isUpdate {
var existingOrder models.OrderDB
if err := tx.First(&existingOrder, order.ID).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "order not found for update")
}
if err := tx.Model(&orderDB).Updates(orderDB).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to update order")
}
if err := tx.Where("order_id = ?", order.ID).Delete(&models.OrderItemDB{}).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to delete existing order items")
}
} else { } else {
if err := tx.Create(&orderDB).Error; err != nil { dbTx = r.db.Begin()
tx.Rollback() 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") return nil, errors.Wrap(err, "failed to insert order")
} }
order.ID = orderDB.ID order.ID = orderDB.ID
} }
var itemIDs []int64 // Only commit if we created the transaction
for i := range order.OrderItems { if tx == nil {
itemIDs = append(itemIDs, order.OrderItems[i].ItemID) if err := dbTx.Commit().Error; err != nil {
} return nil, errors.Wrap(err, "failed to commit transaction")
var products []models.ProductDB
if len(itemIDs) > 0 {
if err := tx.Where("id IN ?", itemIDs).Find(&products).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to fetch products")
} }
} }
productMap := make(map[int64]models.ProductDB) // Return the order with the ID set, but without items (items will be added separately)
for _, product := range products {
productMap[product.ID] = product
}
for i := range order.OrderItems {
item := &order.OrderItems[i]
item.OrderID = orderDB.ID
itemDB := r.toOrderItemDBModel(item)
if err := tx.Create(&itemDB).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to insert order item")
}
item.ID = itemDB.ID
if product, exists := productMap[item.ItemID]; exists {
item.Product = r.toDomainProductModel(&product)
}
}
if err := tx.Commit().Error; err != nil {
return nil, errors.Wrap(err, "failed to commit transaction")
}
return order, nil return order, nil
} }
func (r *orderRepository) toInProgressOrderDBModel(order *entity.Order) models.OrderDB { func (r *orderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
now := time.Now() // Use provided transaction or create new one
var dbTx *gorm.DB
return models.OrderDB{ if tx != nil {
ID: order.ID, dbTx = tx
PartnerID: order.PartnerID, } else {
CustomerID: order.CustomerID, dbTx = r.db.Begin()
CustomerName: order.CustomerName, if dbTx.Error != nil {
PaymentType: order.PaymentType, return errors.Wrap(dbTx.Error, "failed to begin transaction")
PaymentProvider: order.PaymentProvider, }
CreatedBy: order.CreatedBy, defer func() {
CreatedAt: now, if r := recover(); r != nil {
UpdatedAt: now, dbTx.Rollback()
TableNumber: order.TableNumber, }
OrderType: order.OrderType, }()
Status: order.Status,
Amount: order.Amount,
Total: order.Total,
Tax: order.Tax,
Source: order.Source,
} }
for _, item := range items {
itemDB := r.toOrderItemDBModel(&item)
itemDB.OrderID = orderID
if err := dbTx.Create(&itemDB).Error; err != nil {
if tx == nil {
dbTx.Rollback()
}
return errors.Wrap(err, "failed to insert order item")
}
item.ID = itemDB.ID
}
// Only commit if we created the transaction
if tx == nil {
if err := dbTx.Commit().Error; err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
}
return nil
} }
func (r *orderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product { func (r *orderRepository) CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error {
if productDB == nil { itemDB := r.toOrderItemDBModel(item)
return nil itemDB.OrderID = orderID
if err := r.db.Create(&itemDB).Error; err != nil {
return errors.Wrap(err, "failed to insert order item")
} }
return &entity.Product{ item.ID = itemDB.ID
ID: productDB.ID, return nil
Name: productDB.Name,
Description: productDB.Description,
Price: productDB.Price,
CreatedAt: productDB.CreatedAt,
UpdatedAt: productDB.UpdatedAt,
Type: productDB.Type,
Image: productDB.Image,
}
} }
func (r *orderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) { func (r *orderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) {
@ -954,11 +931,6 @@ func (r *orderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64,
order := r.toDomainOrderModel(&orderDB) order := r.toDomainOrderModel(&orderDB)
for _, itemDB := range orderDB.OrderItems {
item := r.toDomainOrderItemModel(&itemDB)
order.OrderItems = append(order.OrderItems, *item)
}
return order, nil return order, nil
} }
@ -1025,3 +997,43 @@ func (r *orderRepository) UpdateOrderTotals(ctx mycontext.Context, orderID int64
return nil return nil
} }
func (r *orderRepository) UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error {
now := time.Now()
result := trx.Model(&models.OrderDB{}).
Where("id = ?", orderID).
Updates(map[string]interface{}{
"amount": amount,
"tax": tax,
"total": total,
"updated_at": now,
})
if result.Error != nil {
return errors.Wrap(result.Error, "failed to update order totals")
}
if result.RowsAffected == 0 {
logger.ContextLogger(ctx).Warn("no order updated")
}
return nil
}
func (r *orderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product {
if productDB == nil {
return nil
}
return &entity.Product{
ID: productDB.ID,
Name: productDB.Name,
Description: productDB.Description,
Price: productDB.Price,
CreatedAt: productDB.CreatedAt,
UpdatedAt: productDB.UpdatedAt,
Type: productDB.Type,
Image: productDB.Image,
}
}

View File

@ -0,0 +1,193 @@
package repository
import (
"github.com/pkg/errors"
"gorm.io/gorm"
)
type QueryBuilder[T any] struct {
db *gorm.DB
model T
}
func NewQueryBuilder[T any](db *gorm.DB) *QueryBuilder[T] {
var model T
return &QueryBuilder[T]{
db: db,
model: model,
}
}
type Filter struct {
Field string
Operator string // "=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "NOT IN", "IS NULL", "IS NOT NULL"
Value interface{}
}
type QueryOptions struct {
Filters []Filter
Limit int
Offset int
OrderBy []string
Preloads []string
GroupBy []string
Having []Filter
Distinct []string
CountOnly bool
}
func (qb *QueryBuilder[T]) BuildQuery(options QueryOptions) *gorm.DB {
query := qb.db.Model(&qb.model)
for _, filter := range options.Filters {
query = qb.applyFilter(query, filter)
}
if len(options.Distinct) > 0 {
for _, distinct := range options.Distinct {
query = query.Distinct(distinct)
}
}
if len(options.GroupBy) > 0 {
for _, groupBy := range options.GroupBy {
query = query.Group(groupBy)
}
}
for _, having := range options.Having {
query = qb.applyFilter(query, having)
}
return query
}
func (qb *QueryBuilder[T]) applyFilter(query *gorm.DB, filter Filter) *gorm.DB {
switch filter.Operator {
case "=", "":
return query.Where(filter.Field+" = ?", filter.Value)
case "!=":
return query.Where(filter.Field+" != ?", filter.Value)
case ">":
return query.Where(filter.Field+" > ?", filter.Value)
case "<":
return query.Where(filter.Field+" < ?", filter.Value)
case ">=":
return query.Where(filter.Field+" >= ?", filter.Value)
case "<=":
return query.Where(filter.Field+" <= ?", filter.Value)
case "LIKE":
return query.Where(filter.Field+" LIKE ?", filter.Value)
case "IN":
return query.Where(filter.Field+" IN ?", filter.Value)
case "NOT IN":
return query.Where(filter.Field+" NOT IN ?", filter.Value)
case "IS NULL":
return query.Where(filter.Field + " IS NULL")
case "IS NOT NULL":
return query.Where(filter.Field + " IS NOT NULL")
case "BETWEEN":
if values, ok := filter.Value.([]interface{}); ok && len(values) == 2 {
return query.Where(filter.Field+" BETWEEN ? AND ?", values[0], values[1])
}
return query
default:
return query.Where(filter.Field+" = ?", filter.Value)
}
}
func (qb *QueryBuilder[T]) ExecuteQuery(baseQuery *gorm.DB, options QueryOptions) *gorm.DB {
query := baseQuery.Session(&gorm.Session{})
if len(options.OrderBy) > 0 {
for _, orderBy := range options.OrderBy {
query = query.Order(orderBy)
}
}
for _, preload := range options.Preloads {
query = query.Preload(preload)
}
if options.Limit > 0 {
query = query.Limit(options.Limit)
}
if options.Offset > 0 {
query = query.Offset(options.Offset)
}
return query
}
func (qb *QueryBuilder[T]) Count(baseQuery *gorm.DB) (int64, error) {
var count int64
if err := baseQuery.Count(&count).Error; err != nil {
return 0, errors.Wrap(err, "failed to count records")
}
return count, nil
}
func (qb *QueryBuilder[T]) Find(query *gorm.DB) ([]T, error) {
var results []T
if err := query.Find(&results).Error; err != nil {
return nil, errors.Wrap(err, "failed to find records")
}
return results, nil
}
func (qb *QueryBuilder[T]) First(query *gorm.DB) (*T, error) {
var result T
if err := query.First(&result).Error; err != nil {
return nil, errors.Wrap(err, "failed to find record")
}
return &result, nil
}
func Equal(field string, value interface{}) Filter {
return Filter{Field: field, Operator: "=", Value: value}
}
func NotEqual(field string, value interface{}) Filter {
return Filter{Field: field, Operator: "!=", Value: value}
}
func GreaterThan(field string, value interface{}) Filter {
return Filter{Field: field, Operator: ">", Value: value}
}
func LessThan(field string, value interface{}) Filter {
return Filter{Field: field, Operator: "<", Value: value}
}
func GreaterEqual(field string, value interface{}) Filter {
return Filter{Field: field, Operator: ">=", Value: value}
}
func LessEqual(field string, value interface{}) Filter {
return Filter{Field: field, Operator: "<=", Value: value}
}
func Like(field string, value string) Filter {
return Filter{Field: field, Operator: "LIKE", Value: value}
}
func In(field string, values interface{}) Filter {
return Filter{Field: field, Operator: "IN", Value: values}
}
func NotIn(field string, values interface{}) Filter {
return Filter{Field: field, Operator: "NOT IN", Value: values}
}
func IsNull(field string) Filter {
return Filter{Field: field, Operator: "IS NULL"}
}
func IsNotNull(field string) Filter {
return Filter{Field: field, Operator: "IS NOT NULL"}
}
func Between(field string, start, end interface{}) Filter {
return Filter{Field: field, Operator: "BETWEEN", Value: []interface{}{start, end}}
}

View File

@ -16,7 +16,6 @@ import (
"enaklo-pos-be/internal/repository/products" "enaklo-pos-be/internal/repository/products"
"enaklo-pos-be/internal/repository/sites" "enaklo-pos-be/internal/repository/sites"
transactions "enaklo-pos-be/internal/repository/transaction" transactions "enaklo-pos-be/internal/repository/transaction"
"enaklo-pos-be/internal/repository/trx"
"enaklo-pos-be/internal/repository/users" "enaklo-pos-be/internal/repository/users"
repository "enaklo-pos-be/internal/repository/wallet" repository "enaklo-pos-be/internal/repository/wallet"
@ -38,7 +37,7 @@ type RepoManagerImpl struct {
OSS OSSRepository OSS OSSRepository
Partner PartnerRepository Partner PartnerRepository
Site SiteRepository Site SiteRepository
Trx TransactionManager Trx Trx
Wallet WalletRepository Wallet WalletRepository
Midtrans Midtrans Midtrans Midtrans
Payment Payment Payment Payment
@ -70,7 +69,7 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig), OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
Partner: partners.NewPartnerRepository(db), Partner: partners.NewPartnerRepository(db),
Site: sites.NewSiteRepository(db), Site: sites.NewSiteRepository(db),
Trx: trx.NewGormTransactionManager(db), Trx: NewTransactionManager(db),
Wallet: repository.NewWalletRepository(db), Wallet: repository.NewWalletRepository(db),
Midtrans: mdtrns.New(&cfg.Midtrans), Midtrans: mdtrns.New(&cfg.Midtrans),
Payment: payment.NewPaymentRepository(db), Payment: payment.NewPaymentRepository(db),
@ -188,12 +187,6 @@ type SiteRepository interface {
SearchSites(ctx context.Context, search *entity.DiscoverySearch) ([]entity.SiteProductInfo, int64, error) SearchSites(ctx context.Context, search *entity.DiscoverySearch) ([]entity.SiteProductInfo, int64, error)
} }
type TransactionManager interface {
Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error)
Commit(session *gorm.DB) *gorm.DB
Rollback(session *gorm.DB) *gorm.DB
}
type WalletRepository interface { type WalletRepository interface {
Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
@ -244,3 +237,9 @@ type PaymentGateway interface {
CreateQRISPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) CreateQRISPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error)
CreatePaymentVA(request entity.PaymentRequest) (*entity.PaymentResponse, error) CreatePaymentVA(request entity.PaymentRequest) (*entity.PaymentResponse, error)
} }
type Trx interface {
Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error)
Commit(session *gorm.DB) *gorm.DB
Rollback(session *gorm.DB) *gorm.DB
}

View File

@ -0,0 +1,32 @@
package repository
import (
"context"
"database/sql"
"gorm.io/gorm"
)
type TransactionManager struct {
db *gorm.DB
}
func NewTransactionManager(db *gorm.DB) *TransactionManager {
return &TransactionManager{db: db}
}
func (tm *TransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) {
tx := tm.db.Begin(opts...)
if tx.Error != nil {
return nil, tx.Error
}
return tx, nil
}
func (tm *TransactionManager) Commit(session *gorm.DB) *gorm.DB {
return session.Commit()
}
func (tm *TransactionManager) Rollback(session *gorm.DB) *gorm.DB {
return session.Rollback()
}

View File

@ -10,9 +10,10 @@ import (
site "enaklo-pos-be/internal/handlers/http/sites" site "enaklo-pos-be/internal/handlers/http/sites"
"enaklo-pos-be/internal/handlers/http/transaction" "enaklo-pos-be/internal/handlers/http/transaction"
"enaklo-pos-be/internal/handlers/http/user" "enaklo-pos-be/internal/handlers/http/user"
"net/http"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
"net/http"
"enaklo-pos-be/internal/middlewares" "enaklo-pos-be/internal/middlewares"

View File

@ -20,13 +20,13 @@ type AuthServiceImpl struct {
user repository.User user repository.User
emailSvc repository.EmailService emailSvc repository.EmailService
emailCfg config.Email emailCfg config.Email
trxRepo repository.TransactionManager trxRepo repository.Trx
license repository.License license repository.License
} }
func New(authRepo repository.Auth, func New(authRepo repository.Auth,
crypto repository.Crypto, user repository.User, emailSvc repository.EmailService, crypto repository.Crypto, user repository.User, emailSvc repository.EmailService,
emailCfg config.Email, trxRepo repository.TransactionManager, emailCfg config.Email, trxRepo repository.Trx,
license repository.License, license repository.License,
) *AuthServiceImpl { ) *AuthServiceImpl {
return &AuthServiceImpl{ return &AuthServiceImpl{

View File

@ -16,14 +16,14 @@ type Config interface {
type BalanceService struct { type BalanceService struct {
repo repository.WalletRepository repo repository.WalletRepository
trx repository.TransactionManager trx repository.Trx
crypt repository.Crypto crypt repository.Crypto
transaction repository.TransactionRepository transaction repository.TransactionRepository
cfg Config cfg Config
} }
func NewBalanceService(repo repository.WalletRepository, func NewBalanceService(repo repository.WalletRepository,
trx repository.TransactionManager, trx repository.Trx,
crypt repository.Crypto, cfg Config, crypt repository.Crypto, cfg Config,
transaction repository.TransactionRepository) *BalanceService { transaction repository.TransactionRepository) *BalanceService {
return &BalanceService{ return &BalanceService{

View File

@ -12,14 +12,14 @@ import (
type PartnerService struct { type PartnerService struct {
repo repository.PartnerRepository repo repository.PartnerRepository
trx repository.TransactionManager trx repository.Trx
userSvc *users.UserService userSvc *users.UserService
walletRepo repository.WalletRepository walletRepo repository.WalletRepository
userRepo repository.User userRepo repository.User
} }
func NewPartnerService(repo repository.PartnerRepository, func NewPartnerService(repo repository.PartnerRepository,
userSvc *users.UserService, repoManager repository.TransactionManager, userSvc *users.UserService, repoManager repository.Trx,
walletRepo repository.WalletRepository, walletRepo repository.WalletRepository,
userRepo repository.User, userRepo repository.User,
) *PartnerService { ) *PartnerService {

View File

@ -63,7 +63,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
productSvcV2, custSvcV2, repo.TransactionRepo, productSvcV2, custSvcV2, repo.TransactionRepo,
repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings, repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings,
repo.UndianRepository, cashierSvc) repo.UndianRepository, cashierSvc)
inprogressOrder := inprogress_order.NewInProgressOrderService(repo.OrderRepo, orderService, productSvcV2) inprogressOrder := inprogress_order.NewInProgressOrderService(repo.OrderRepo, orderService, productSvcV2, repo.Trx)
categorySvc := category.New(repo.CategoryRepository) categorySvc := category.New(repo.CategoryRepository)
return &ServiceManagerImpl{ return &ServiceManagerImpl{
AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License), AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License),

View File

@ -13,12 +13,12 @@ import (
type TransactionService struct { type TransactionService struct {
repo repository.TransactionRepository repo repository.TransactionRepository
wallet repository.WalletRepository wallet repository.WalletRepository
trx repository.TransactionManager trx repository.Trx
} }
func New(repo repository.TransactionRepository, func New(repo repository.TransactionRepository,
wallet repository.WalletRepository, wallet repository.WalletRepository,
trx repository.TransactionManager, trx repository.Trx,
) *TransactionService { ) *TransactionService {
return &TransactionService{ return &TransactionService{
repo: repo, repo: repo,

View File

@ -4,6 +4,7 @@ import (
"enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -13,6 +14,7 @@ type Service interface {
CloseSession(ctx mycontext.Context, sessionID int64, closingAmount float64) (*entity.CashierSessionReport, error) CloseSession(ctx mycontext.Context, sessionID int64, closingAmount float64) (*entity.CashierSessionReport, error)
GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
GetSessionReport(ctx mycontext.Context, sessionID int64) (*entity.CashierSessionReport, error) GetSessionReport(ctx mycontext.Context, sessionID int64) (*entity.CashierSessionReport, error)
GetSessionHistory(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error)
} }
type Repository interface { type Repository interface {
@ -21,6 +23,7 @@ type Repository interface {
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error) GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error)
GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error) GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error)
GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error)
} }
type cashierSessionSvc struct { type cashierSessionSvc struct {
@ -97,3 +100,11 @@ func (s *cashierSessionSvc) GetSessionReport(ctx mycontext.Context, sessionID in
Payments: report, Payments: report,
}, nil }, nil
} }
func (s *cashierSessionSvc) GetSessionHistory(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) {
sessions, total, err := s.repo.GetSessionHistoryByPartnerID(ctx, partnerID, limit, offset)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to get session history")
}
return sessions, total, nil
}

View File

@ -1,11 +1,17 @@
package inprogress_order package inprogress_order
import ( import (
"context"
"database/sql"
"enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
order2 "enaklo-pos-be/internal/constants/order" order2 "enaklo-pos-be/internal/constants/order"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/services/v2/order" "enaklo-pos-be/internal/services/v2/order"
"fmt"
"gorm.io/gorm"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -19,56 +25,185 @@ type InProgressOrderService interface {
type OrderRepository interface { type OrderRepository interface {
FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error)
CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error)
CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error
} }
type OrderCalculator interface { type OrderCalculator interface {
CalculateOrderTotals( CalculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, partnerID int64) (*entity.OrderCalculation, error)
ctx mycontext.Context,
items []entity.OrderItemRequest,
productDetails *entity.ProductDetails,
source string,
partnerID int64,
) (*entity.OrderCalculation, error)
ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error)
} }
type TransactionManager interface {
Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error)
Commit(session *gorm.DB) *gorm.DB
Rollback(session *gorm.DB) *gorm.DB
}
type inProgressOrderSvc struct { type inProgressOrderSvc struct {
repo OrderRepository repo OrderRepository
orderCalculator OrderCalculator orderCalculator OrderCalculator
product order.ProductService product order.ProductService
trx TransactionManager
} }
func NewInProgressOrderService(repo OrderRepository, calculator OrderCalculator, product order.ProductService) InProgressOrderService { func NewInProgressOrderService(repo OrderRepository,
calculator OrderCalculator, product order.ProductService, trx TransactionManager) InProgressOrderService {
return &inProgressOrderSvc{ return &inProgressOrderSvc{
repo: repo, repo: repo,
orderCalculator: calculator, orderCalculator: calculator,
product: product, product: product,
trx: trx,
} }
} }
func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) { func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) {
productIDs, filteredItems, err := s.orderCalculator.ValidateOrderItems(ctx, req.OrderItems) orderItems, err := s.prepareOrderItems(ctx, req.OrderItems, req.PartnerID)
if err != nil {
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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
orderItems := make([]entity.OrderItem, len(req.OrderItems)) orderCalculation, err := s.calculateOrderTotals(ctx, req.OrderItems, req.Source, req.PartnerID)
for i, item := range req.OrderItems { 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] product, exists := productDetails.Products[item.ProductID]
productName := "" productName := ""
if exists { if exists {
@ -76,17 +211,34 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques
} }
orderItems[i] = entity.OrderItem{ orderItems[i] = entity.OrderItem{
ItemID: item.ProductID, ItemID: item.ProductID,
ItemName: productName, ItemName: productName,
Quantity: item.Quantity, Quantity: item.Quantity,
Price: product.Price, Price: product.Price,
ItemType: product.Type, ItemType: product.Type,
Description: product.Description, Notes: item.Notes,
Notes: item.Notes,
} }
} }
order := &entity.Order{ return orderItems, nil
}
func (s *inProgressOrderSvc) calculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, source string, partnerID int64) (*entity.OrderCalculation, error) {
productIDs, _, err := s.orderCalculator.ValidateOrderItems(ctx, items)
if err != nil {
return nil, err
}
productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID)
if err != nil {
return nil, err
}
return s.orderCalculator.CalculateOrderTotals(ctx, items, productDetails, source, partnerID)
}
func (s *inProgressOrderSvc) createOrderEntity(req *entity.OrderRequest, orderItems []entity.OrderItem, calculation *entity.OrderCalculation) *entity.Order {
return &entity.Order{
ID: req.ID, ID: req.ID,
PartnerID: req.PartnerID, PartnerID: req.PartnerID,
CustomerID: req.CustomerID, CustomerID: req.CustomerID,
@ -95,22 +247,50 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques
OrderItems: orderItems, OrderItems: orderItems,
TableNumber: req.TableNumber, TableNumber: req.TableNumber,
OrderType: req.OrderType, OrderType: req.OrderType,
Total: orderCalculation.Total, Total: calculation.Total,
Tax: orderCalculation.Tax, Tax: calculation.Tax,
Amount: orderCalculation.Subtotal, Amount: calculation.Subtotal,
Status: order2.Pending.String(), Status: order2.Pending.String(),
Source: req.Source, Source: req.Source,
} }
}
createdOrder, err := s.repo.CreateOrUpdate(ctx, order) func (s *inProgressOrderSvc) convertToOrderItemRequests(items []entity.OrderItem) []entity.OrderItemRequest {
if err != nil { requests := make([]entity.OrderItemRequest, len(items))
logger.ContextLogger(ctx).Error("failed to create in-progress order", for i, item := range items {
zap.Error(err), requests[i] = entity.OrderItemRequest{
zap.Int64("partnerID", order.PartnerID)) ProductID: item.ItemID,
return nil, errors.Wrap(err, "failed to create in-progress order") Quantity: item.Quantity,
Notes: item.Notes,
}
}
return requests
}
func (s *inProgressOrderSvc) extractNewlyAddedItems(updatedOrder *entity.Order, existingItems []entity.OrderItem) []entity.OrderItem {
if len(existingItems) == 0 {
return updatedOrder.OrderItems
} }
return createdOrder, nil existingItemMap := make(map[string]struct{})
for _, item := range existingItems {
key := s.createItemKey(item)
existingItemMap[key] = struct{}{}
}
newlyAdded := make([]entity.OrderItem, 0)
for _, item := range updatedOrder.OrderItems {
key := s.createItemKey(item)
if _, exists := existingItemMap[key]; !exists {
newlyAdded = append(newlyAdded, item)
}
}
return newlyAdded
}
func (s *inProgressOrderSvc) createItemKey(item entity.OrderItem) string {
return fmt.Sprintf("%d_%s", item.ItemID, item.Notes)
} }
func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) { func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) {
@ -127,79 +307,6 @@ func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partner
return orders, nil return orders, nil
} }
func (s *inProgressOrderSvc) AddItems(ctx mycontext.Context, orderID int64, newItems []entity.OrderItemRequest) (*entity.Order, error) {
existingOrder, err := s.repo.FindByID(ctx, orderID)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch order %d", orderID)
}
type itemKey struct {
ProductID int64
Notes string
}
itemMap := make(map[itemKey]entity.OrderItemRequest)
existingKeys := make(map[itemKey]struct{})
// Collect existing items
for _, oi := range existingOrder.OrderItems {
key := itemKey{ProductID: oi.ItemID, Notes: oi.Notes}
existingKeys[key] = struct{}{}
itemMap[key] = entity.OrderItemRequest{
ProductID: oi.ItemID,
Quantity: oi.Quantity,
Notes: oi.Notes,
}
}
// Merge new items into map
for _, ni := range newItems {
key := itemKey{ProductID: ni.ProductID, Notes: ni.Notes}
if existing, found := itemMap[key]; found {
existing.Quantity += ni.Quantity
itemMap[key] = existing
} else {
itemMap[key] = ni
}
}
// Prepare merged items
mergedItems := make([]entity.OrderItemRequest, 0, len(itemMap))
for _, item := range itemMap {
mergedItems = append(mergedItems, item)
}
// Save updated order
req := &entity.OrderRequest{
ID: existingOrder.ID,
PartnerID: existingOrder.PartnerID,
CustomerID: existingOrder.CustomerID,
CustomerName: existingOrder.CustomerName,
CreatedBy: existingOrder.CreatedBy,
TableNumber: existingOrder.TableNumber,
OrderType: existingOrder.OrderType,
Source: existingOrder.Source,
OrderItems: mergedItems,
}
savedOrder, err := s.Save(ctx, req)
if err != nil {
return nil, err
}
newlyAdded := make([]entity.OrderItem, 0)
for _, item := range savedOrder.OrderItems {
key := itemKey{ProductID: item.ItemID, Notes: item.Notes}
if _, exists := existingKeys[key]; !exists {
newlyAdded = append(newlyAdded, item)
}
}
savedOrder.OrderItems = newlyAdded
return savedOrder, nil
}
func (s *inProgressOrderSvc) GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) { func (s *inProgressOrderSvc) GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) {
orders, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) orders, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
if err != nil { if err != nil {

View File

@ -0,0 +1,898 @@
package inprogress_order
import (
"context"
"database/sql"
"enaklo-pos-be/internal/common/mycontext"
order2 "enaklo-pos-be/internal/constants/order"
"enaklo-pos-be/internal/entity"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"gorm.io/gorm"
)
// Mock implementations
type MockOrderRepository struct {
mock.Mock
}
func (m *MockOrderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.Order), args.Error(1)
}
func (m *MockOrderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
args := m.Called(ctx, order, tx)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.Order), args.Error(1)
}
func (m *MockOrderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
args := m.Called(ctx, orderID, items, tx)
return args.Error(0)
}
func (m *MockOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) {
args := m.Called(ctx, partnerID, limit, offset, status)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*entity.Order), args.Error(1)
}
func (m *MockOrderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) {
args := m.Called(ctx, id, partnerID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.Order), args.Error(1)
}
type MockOrderCalculator struct {
mock.Mock
}
func (m *MockOrderCalculator) CalculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, partnerID int64) (*entity.OrderCalculation, error) {
args := m.Called(ctx, items, productDetails, source, partnerID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.OrderCalculation), args.Error(1)
}
func (m *MockOrderCalculator) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) {
args := m.Called(ctx, items)
// Handle nil values properly
var productIDs []int64
if args.Get(0) != nil {
productIDs = args.Get(0).([]int64)
}
var filteredItems []entity.OrderItemRequest
if args.Get(1) != nil {
filteredItems = args.Get(1).([]entity.OrderItemRequest)
}
return productIDs, filteredItems, args.Error(2)
}
type MockProductService struct {
mock.Mock
}
func (m *MockProductService) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) {
args := m.Called(ctx, productIDs, partnerID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.ProductDetails), args.Error(1)
}
func (m *MockProductService) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) {
args := m.Called(ctx, ids, partnerID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*entity.Product), args.Error(1)
}
type MockTransactionManager struct {
mock.Mock
}
func (m *MockTransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) {
args := m.Called(ctx, opts)
return args.Get(0).(*gorm.DB), args.Error(1)
}
func (m *MockTransactionManager) Commit(session *gorm.DB) *gorm.DB {
args := m.Called(session)
return args.Get(0).(*gorm.DB)
}
func (m *MockTransactionManager) Rollback(session *gorm.DB) *gorm.DB {
args := m.Called(session)
return args.Get(0).(*gorm.DB)
}
func TestInProgressOrderService_Save(t *testing.T) {
tests := []struct {
name string
request *entity.OrderRequest
setupMocks func(*MockOrderRepository, *MockOrderCalculator, *MockProductService)
expectedResult *entity.Order
expectedError string
}{
{
name: "successful order creation",
request: &entity.OrderRequest{
ID: 1,
PartnerID: 100,
CustomerID: func() *int64 { id := int64(200); return &id }(),
CustomerName: "John Doe",
CreatedBy: 300,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 2, Notes: "Extra spicy"},
{ProductID: 2, Quantity: 1, Notes: ""},
},
TableNumber: "A1",
OrderType: "DINE_IN",
Source: "POS",
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
// Mock ValidateOrderItems
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1, 2},
[]entity.OrderItemRequest{
{ProductID: 1, Quantity: 2, Notes: "Extra spicy"},
{ProductID: 2, Quantity: 1, Notes: ""},
},
nil,
)
// Mock GetProductDetails
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
2: {ID: 2, Name: "Fries", Price: 5.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1, 2}, int64(100)).Return(productDetails, nil)
// Mock CalculateOrderTotals
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "POS", int64(100)).Return(
&entity.OrderCalculation{
Subtotal: 25.0,
Tax: 2.5,
Total: 27.5,
},
nil,
)
// Mock CreateOrder (returns order without items)
createdOrder := &entity.Order{
ID: 1,
PartnerID: 100,
CustomerID: func() *int64 { id := int64(200); return &id }(),
CustomerName: "John Doe",
CreatedBy: 300,
TableNumber: "A1",
OrderType: "DINE_IN",
Total: 27.5,
Tax: 2.5,
Amount: 25.0,
Status: order2.Pending.String(),
Source: "POS",
}
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(createdOrder, nil)
// Mock CreateOrderItems
repo.On("CreateOrderItems", mock.Anything, int64(1), mock.Anything).Return(nil)
// Mock FindByID (returns full order with items)
expectedOrder := &entity.Order{
ID: 1,
PartnerID: 100,
CustomerID: func() *int64 { id := int64(200); return &id }(),
CustomerName: "John Doe",
CreatedBy: 300,
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
{ItemID: 2, ItemName: "Fries", Quantity: 1, Price: 5.0, ItemType: "PRODUCT", Notes: ""},
},
TableNumber: "A1",
OrderType: "DINE_IN",
Total: 27.5,
Tax: 2.5,
Amount: 25.0,
Status: order2.Pending.String(),
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(expectedOrder, nil)
},
expectedResult: &entity.Order{
ID: 1,
PartnerID: 100,
CustomerID: func() *int64 { id := int64(200); return &id }(),
CustomerName: "John Doe",
CreatedBy: 300,
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
{ItemID: 2, ItemName: "Fries", Quantity: 1, Price: 5.0, ItemType: "PRODUCT", Notes: ""},
},
TableNumber: "A1",
OrderType: "DINE_IN",
Total: 27.5,
Tax: 2.5,
Amount: 25.0,
Status: order2.Pending.String(),
Source: "POS",
},
expectedError: "",
},
{
name: "validation error",
request: &entity.OrderRequest{
PartnerID: 100,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 0}, // Invalid quantity
},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
nil, nil, errors.New("invalid quantity"),
)
},
expectedResult: nil,
expectedError: "invalid quantity",
},
{
name: "product details error",
request: &entity.OrderRequest{
PartnerID: 100,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(nil, errors.New("product not found"))
},
expectedResult: nil,
expectedError: "product not found",
},
{
name: "calculation error",
request: &entity.OrderRequest{
PartnerID: 100,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "", int64(100)).Return(
nil, errors.New("calculation failed"),
)
},
expectedResult: nil,
expectedError: "calculation failed",
},
{
name: "repository error",
request: &entity.OrderRequest{
PartnerID: 100,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "", int64(100)).Return(
&entity.OrderCalculation{Subtotal: 10.0, Tax: 1.0, Total: 11.0},
nil,
)
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(nil, errors.New("database error"))
},
expectedResult: nil,
expectedError: "failed to create in-progress order: database error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mocks
mockRepo := &MockOrderRepository{}
mockCalc := &MockOrderCalculator{}
mockProd := &MockProductService{}
if tt.setupMocks != nil {
tt.setupMocks(mockRepo, mockCalc, mockProd)
}
// Create service
service := NewInProgressOrderService(mockRepo, mockCalc, mockProd)
// Execute
ctx := mycontext.NewContext(context.Background())
result, err := service.Save(ctx, tt.request)
// Assert
if tt.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.expectedResult.ID, result.ID)
assert.Equal(t, tt.expectedResult.PartnerID, result.PartnerID)
assert.Equal(t, tt.expectedResult.Status, result.Status)
assert.Equal(t, tt.expectedResult.Total, result.Total)
assert.Len(t, result.OrderItems, len(tt.expectedResult.OrderItems))
}
// Verify all mocks were called as expected
mockRepo.AssertExpectations(t)
mockCalc.AssertExpectations(t)
mockProd.AssertExpectations(t)
})
}
}
func TestInProgressOrderService_AddItems(t *testing.T) {
tests := []struct {
name string
orderID int64
newItems []entity.OrderItemRequest
setupMocks func(*MockOrderRepository, *MockOrderCalculator, *MockProductService)
expectedResult *entity.Order
expectedError string
}{
{
name: "successful add items to pending order",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 3, Quantity: 1, Notes: "No onions"},
{ProductID: 4, Quantity: 2, Notes: ""},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
// Mock existing order
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil).Once()
// Mock ValidateOrderItems for new items
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{3, 4},
[]entity.OrderItemRequest{
{ProductID: 3, Quantity: 1, Notes: "No onions"},
{ProductID: 4, Quantity: 2, Notes: ""},
},
nil,
)
// Mock GetProductDetails
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
3: {ID: 3, Name: "Salad", Price: 8.0, Type: "PRODUCT"},
4: {ID: 4, Name: "Drink", Price: 3.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{3, 4}, int64(100)).Return(productDetails, nil)
// Mock CreateOrderItems
repo.On("CreateOrderItems", mock.Anything, int64(1), mock.Anything).Return(nil)
// Mock FindByID (returns updated order with all items)
updatedOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
{ItemID: 3, ItemName: "Salad", Quantity: 1, Price: 8.0, ItemType: "PRODUCT", Notes: "No onions"},
{ItemID: 4, ItemName: "Drink", Quantity: 2, Price: 3.0, ItemType: "PRODUCT", Notes: ""},
},
Total: 40.7,
Tax: 3.7,
Amount: 37.0,
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(updatedOrder, nil).Once()
// Mock CalculateOrderTotals for combined items
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
&entity.OrderCalculation{
Subtotal: 37.0,
Tax: 3.7,
Total: 40.7,
},
nil,
)
// Mock CreateOrder for updating totals
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(updatedOrder, nil)
},
expectedResult: &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 3, ItemName: "Salad", Quantity: 1, Price: 8.0, ItemType: "PRODUCT", Notes: "No onions"},
{ItemID: 4, ItemName: "Drink", Quantity: 2, Price: 3.0, ItemType: "PRODUCT", Notes: ""},
},
Total: 40.7,
Tax: 3.7,
Amount: 37.0,
Source: "POS",
},
expectedError: "",
},
{
name: "order not found",
orderID: 999,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
repo.On("FindByID", mock.Anything, int64(999)).Return(nil, errors.New("order not found"))
},
expectedResult: nil,
expectedError: "failed to fetch order 999: order not found",
},
{
name: "order not in pending status",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
Status: order2.Paid.String(),
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
},
expectedResult: nil,
expectedError: "cannot add items to order with status PAID",
},
{
name: "validation error for new items",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 0}, // Invalid quantity
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
nil, nil, errors.New("invalid quantity"),
)
},
expectedResult: nil,
expectedError: "invalid quantity",
},
{
name: "product details error",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(nil, errors.New("product not found"))
},
expectedResult: nil,
expectedError: "product not found",
},
{
name: "calculation error",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
nil, errors.New("calculation failed"),
)
},
expectedResult: nil,
expectedError: "calculation failed",
},
{
name: "repository update error",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
&entity.OrderCalculation{Subtotal: 30.0, Tax: 3.0, Total: 33.0},
nil,
)
repo.On("CreateOrderItems", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("database error"))
},
expectedResult: nil,
expectedError: "failed to add order items: database error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mocks
mockRepo := &MockOrderRepository{}
mockCalc := &MockOrderCalculator{}
mockProd := &MockProductService{}
if tt.setupMocks != nil {
tt.setupMocks(mockRepo, mockCalc, mockProd)
}
// Create service
service := NewInProgressOrderService(mockRepo, mockCalc, mockProd)
// Execute
ctx := mycontext.NewContext(context.Background())
result, err := service.AddItems(ctx, tt.orderID, tt.newItems)
// Assert
if tt.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.expectedResult.ID, result.ID)
assert.Equal(t, tt.expectedResult.PartnerID, result.PartnerID)
assert.Equal(t, tt.expectedResult.Status, result.Status)
assert.Equal(t, tt.expectedResult.Total, result.Total)
// Should only return newly added items
assert.Len(t, result.OrderItems, len(tt.expectedResult.OrderItems))
}
// Verify all mocks were called as expected
mockRepo.AssertExpectations(t)
mockCalc.AssertExpectations(t)
mockProd.AssertExpectations(t)
})
}
}
func TestInProgressOrderService_HelperMethods(t *testing.T) {
service := &inProgressOrderSvc{}
t.Run("convertToOrderItemRequests", func(t *testing.T) {
items := []entity.OrderItem{
{ItemID: 1, Quantity: 2, Notes: "Extra spicy"},
{ItemID: 2, Quantity: 1, Notes: ""},
}
result := service.convertToOrderItemRequests(items)
assert.Len(t, result, 2)
assert.Equal(t, int64(1), result[0].ProductID)
assert.Equal(t, 2, result[0].Quantity)
assert.Equal(t, "Extra spicy", result[0].Notes)
assert.Equal(t, int64(2), result[1].ProductID)
assert.Equal(t, 1, result[1].Quantity)
assert.Equal(t, "", result[1].Notes)
})
t.Run("createItemKey", func(t *testing.T) {
item := entity.OrderItem{ItemID: 1, Notes: "Extra spicy"}
key := service.createItemKey(item)
assert.Equal(t, "1_Extra spicy", key)
item2 := entity.OrderItem{ItemID: 2, Notes: ""}
key2 := service.createItemKey(item2)
assert.Equal(t, "2_", key2)
})
t.Run("extractNewlyAddedItems", func(t *testing.T) {
existingItems := []entity.OrderItem{
{ItemID: 1, Notes: "Extra spicy"},
{ItemID: 2, Notes: ""},
}
updatedOrder := &entity.Order{
OrderItems: []entity.OrderItem{
{ItemID: 1, Notes: "Extra spicy"},
{ItemID: 2, Notes: ""},
{ItemID: 3, Notes: "No onions"},
{ItemID: 4, Notes: ""},
},
}
result := service.extractNewlyAddedItems(updatedOrder, existingItems)
assert.Len(t, result, 2)
assert.Equal(t, int64(3), result[0].ItemID)
assert.Equal(t, "No onions", result[0].Notes)
assert.Equal(t, int64(4), result[1].ItemID)
assert.Equal(t, "", result[1].Notes)
})
t.Run("extractNewlyAddedItems with no existing items", func(t *testing.T) {
updatedOrder := &entity.Order{
OrderItems: []entity.OrderItem{
{ItemID: 1, Notes: "Extra spicy"},
{ItemID: 2, Notes: ""},
},
}
result := service.extractNewlyAddedItems(updatedOrder, []entity.OrderItem{})
assert.Len(t, result, 2)
assert.Equal(t, int64(1), result[0].ItemID)
assert.Equal(t, int64(2), result[1].ItemID)
})
}
func TestSave_WithTransaction(t *testing.T) {
// Setup
mockRepo := new(MockOrderRepository)
mockCalculator := new(MockOrderCalculator)
mockProduct := new(MockProductService)
mockTrx := new(MockTransactionManager)
service := NewInProgressOrderService(mockRepo, mockCalculator, mockProduct, mockTrx)
ctx := mycontext.NewContext(context.Background())
req := &entity.OrderRequest{
PartnerID: 1,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 2},
},
Source: "pos",
}
// Mock transaction
mockTx := &gorm.DB{}
mockTrx.On("Begin", ctx, mock.Anything).Return(mockTx, nil)
mockTrx.On("Commit", mockTx).Return(mockTx)
mockTrx.On("Rollback", mockTx).Return(mockTx)
// Mock calculator
productIDs := []int64{1}
filteredItems := []entity.OrderItemRequest{{ProductID: 1, Quantity: 2}}
mockCalculator.On("ValidateOrderItems", ctx, req.OrderItems).Return(productIDs, filteredItems, nil)
// Mock product service
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Test Product", Price: 10.0},
},
}
mockProduct.On("GetProductDetails", ctx, productIDs, req.PartnerID).Return(productDetails, nil)
// Mock calculation
calculation := &entity.OrderCalculation{
Subtotal: 20.0,
Tax: 2.0,
Total: 22.0,
}
mockCalculator.On("CalculateOrderTotals", ctx, req.OrderItems, productDetails, req.Source, req.PartnerID).Return(calculation, nil)
// Mock repository calls
createdOrder := &entity.Order{ID: 1, PartnerID: 1}
mockRepo.On("CreateOrder", ctx, mock.AnythingOfType("*entity.Order"), mockTx).Return(createdOrder, nil)
mockRepo.On("CreateOrderItems", ctx, int64(1), mock.AnythingOfType("[]entity.OrderItem"), mockTx).Return(nil)
fullOrder := &entity.Order{ID: 1, PartnerID: 1, OrderItems: []entity.OrderItem{}}
mockRepo.On("FindByID", ctx, int64(1)).Return(fullOrder, nil)
// Execute
result, err := service.Save(ctx, req)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, int64(1), result.ID)
// Verify all mocks were called
mockTrx.AssertExpectations(t)
mockRepo.AssertExpectations(t)
mockCalculator.AssertExpectations(t)
mockProduct.AssertExpectations(t)
}
func TestAddItems_WithTransaction(t *testing.T) {
// Setup
mockRepo := new(MockOrderRepository)
mockCalculator := new(MockOrderCalculator)
mockProduct := new(MockProductService)
mockTrx := new(MockTransactionManager)
service := NewInProgressOrderService(mockRepo, mockCalculator, mockProduct, mockTrx)
ctx := mycontext.NewContext(context.Background())
orderID := int64(1)
newItems := []entity.OrderItemRequest{
{ProductID: 2, Quantity: 1},
}
// Mock existing order
existingOrder := &entity.Order{
ID: orderID,
Status: "pending",
OrderItems: []entity.OrderItem{
{ItemID: 1, Quantity: 2},
},
}
mockRepo.On("FindByID", ctx, orderID).Return(existingOrder, nil)
// Mock transaction
mockTx := &gorm.DB{}
mockTrx.On("Begin", ctx, mock.Anything).Return(mockTx, nil)
mockTrx.On("Commit", mockTx).Return(mockTx)
mockTrx.On("Rollback", mockTx).Return(mockTx)
// Mock calculator
productIDs := []int64{2}
filteredItems := []entity.OrderItemRequest{{ProductID: 2, Quantity: 1}}
mockCalculator.On("ValidateOrderItems", ctx, newItems).Return(productIDs, filteredItems, nil)
// Mock product service
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
2: {ID: 2, Name: "New Product", Price: 15.0},
},
}
mockProduct.On("GetProductDetails", ctx, productIDs, existingOrder.PartnerID).Return(productDetails, nil)
// Mock repository calls
mockRepo.On("CreateOrderItems", ctx, orderID, mock.AnythingOfType("[]entity.OrderItem"), mockTx).Return(nil)
updatedOrder := &entity.Order{
ID: orderID,
Status: "pending",
OrderItems: []entity.OrderItem{
{ItemID: 1, Quantity: 2},
{ItemID: 2, Quantity: 1},
},
}
mockRepo.On("FindByID", ctx, orderID).Return(updatedOrder, nil)
// Mock calculation for updated totals
combinedItems := []entity.OrderItemRequest{
{ProductID: 1, Quantity: 2},
{ProductID: 2, Quantity: 1},
}
updatedCalculation := &entity.OrderCalculation{
Subtotal: 35.0,
Tax: 3.5,
Total: 38.5,
}
mockCalculator.On("CalculateOrderTotals", ctx, combinedItems, productDetails, updatedOrder.Source, updatedOrder.PartnerID).Return(updatedCalculation, nil)
updatedOrderWithTotals := &entity.Order{
ID: orderID,
Status: "pending",
Total: 38.5,
Tax: 3.5,
Amount: 35.0,
OrderItems: []entity.OrderItem{
{ItemID: 1, Quantity: 2},
{ItemID: 2, Quantity: 1},
},
}
mockRepo.On("CreateOrder", ctx, mock.AnythingOfType("*entity.Order"), mockTx).Return(updatedOrderWithTotals, nil)
// Execute
result, err := service.AddItems(ctx, orderID, newItems)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, orderID, result.ID)
// Verify all mocks were called
mockTrx.AssertExpectations(t)
mockRepo.AssertExpectations(t)
mockCalculator.AssertExpectations(t)
mockProduct.AssertExpectations(t)
}

View File

@ -5,6 +5,7 @@ import (
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"fmt" "fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -108,25 +109,61 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
return err return err
} }
// Only allow voiding for NEW, PENDING orders
if order.Status != "NEW" && order.Status != "PENDING" { if order.Status != "NEW" && order.Status != "PENDING" {
return errors.New("only new or pending orders can be voided") return errors.New("only new or pending orders can be voided")
} }
if voidType == "ALL" { if voidType == "ALL" {
// Void entire order // Void all items - create new VOIDED items for all existing items
for _, orderItem := range order.OrderItems {
if orderItem.Status == "ACTIVE" && orderItem.Quantity > 0 {
// Create new VOIDED order item with the voided quantity
voidedItem := &entity.OrderItem{
OrderID: orderID,
ItemID: orderItem.ItemID,
ItemType: orderItem.ItemType,
Price: orderItem.Price,
Quantity: orderItem.Quantity, // Void the full quantity
Status: "VOIDED",
CreatedBy: orderItem.CreatedBy,
ItemName: orderItem.ItemName,
Notes: reason, // Use the reason as notes for tracking
}
err = s.repo.CreateOrderItem(ctx, orderID, voidedItem)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create voided order item", zap.Error(err))
return err
}
// Update original item quantity to 0
err = s.repo.UpdateOrderItem(ctx, orderItem.ID, 0)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update original order item", zap.Error(err))
return err
}
}
}
// Update order status to VOIDED
err = s.repo.UpdateOrder(ctx, orderID, "VOIDED", reason) err = s.repo.UpdateOrder(ctx, orderID, "VOIDED", reason)
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("failed to void order", zap.Error(err)) logger.ContextLogger(ctx).Error("failed to void order", zap.Error(err))
return err return err
} }
// Recalculate order totals (should be 0 for voided order)
err = s.repo.UpdateOrderTotals(ctx, orderID, 0, 0, 0)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
return err
}
} else if voidType == "ITEM" { } else if voidType == "ITEM" {
// Void specific items // Void specific items
voidedAmount := 0.0
orderItemMap := make(map[int64]*entity.OrderItem) orderItemMap := make(map[int64]*entity.OrderItem)
for i := range order.OrderItems {
for _, item := range order.OrderItems { orderItemMap[order.OrderItems[i].ID] = &order.OrderItems[i]
orderItemMap[item.ID] = &item
} }
for _, voidItem := range items { for _, voidItem := range items {
@ -135,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)) return errors.New(fmt.Sprintf("order item %d not found", voidItem.OrderItemID))
} }
if orderItem.Status != "ACTIVE" {
return errors.New(fmt.Sprintf("order item %d is not active", voidItem.OrderItemID))
}
if voidItem.Quantity > orderItem.Quantity { if voidItem.Quantity > orderItem.Quantity {
return errors.New(fmt.Sprintf("void quantity %d exceeds available quantity %d for item %d", return errors.New(fmt.Sprintf("void quantity %d exceeds available quantity %d for item %d",
voidItem.Quantity, orderItem.Quantity, voidItem.OrderItemID)) voidItem.Quantity, orderItem.Quantity, voidItem.OrderItemID))
} }
voidedAmount += orderItem.Price * float64(voidItem.Quantity)
} }
// Update order items with reduced quantities
for _, voidItem := range items { for _, voidItem := range items {
orderItem := orderItemMap[voidItem.OrderItemID] orderItem := orderItemMap[voidItem.OrderItemID]
newQuantity := orderItem.Quantity - voidItem.Quantity
if newQuantity == 0 { // Create new VOIDED order item with the voided quantity
// Remove item completely voidedItem := &entity.OrderItem{
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, 0) OrderID: orderID,
} else { ItemID: orderItem.ItemID,
// Update quantity ItemType: orderItem.ItemType,
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity) Price: orderItem.Price,
Quantity: voidItem.Quantity, // Void the requested quantity
Status: "VOIDED",
CreatedBy: orderItem.CreatedBy,
ItemName: orderItem.ItemName,
Notes: reason, // Use the reason as notes for tracking
} }
err = s.repo.CreateOrderItem(ctx, orderID, voidedItem)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create voided order item", zap.Error(err))
return err
}
// Update original item quantity
newQuantity := orderItem.Quantity - voidItem.Quantity
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity)
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err)) logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err))
return err return err
} }
} }
// Recalculate order totals updatedOrder, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
remainingAmount := order.Amount - voidedAmount
remainingTax := (remainingAmount / order.Amount) * order.Tax
remainingTotal := remainingAmount + remainingTax
// Update order totals
err = s.repo.UpdateOrderTotals(ctx, orderID, remainingAmount, remainingTax, remainingTotal)
if err != nil { 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 return err
} }
// Update order status to PARTIAL if some items remain, otherwise to VOIDED var activeItems []entity.OrderItemRequest
newStatus := "PARTIAL" for _, item := range updatedOrder.OrderItems {
if remainingAmount <= 0 { if item.Status == "ACTIVE" && item.Quantity > 0 {
newStatus = "VOIDED" activeItems = append(activeItems, entity.OrderItemRequest{
ProductID: item.ItemID,
Quantity: item.Quantity,
Notes: item.Notes,
})
}
} }
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason) if len(activeItems) > 0 {
if err != nil { productIDs, _, err := s.ValidateOrderItems(ctx, activeItems)
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err)) if err != nil {
return err 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 return nil
} }
// SplitBillRequest handles splitting bills by items or amounts func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("failed to find order for split bill", zap.Error(err)) logger.ContextLogger(ctx).Error("failed to find order for split bill", zap.Error(err))
@ -210,9 +305,9 @@ func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID in
var splitOrder *entity.Order var splitOrder *entity.Order
if splitType == "ITEM" { if splitType == "ITEM" {
splitOrder, err = s.splitByItems(ctx, order, paymentMethod, paymentProvider, items) splitOrder, err = s.splitByItems(ctx, order, items)
} else if splitType == "AMOUNT" { } else if splitType == "AMOUNT" {
splitOrder, err = s.splitByAmount(ctx, order, paymentMethod, paymentProvider, amount) splitOrder, err = s.splitByAmount(ctx, order, amount)
} }
if err != nil { if err != nil {
@ -228,12 +323,12 @@ func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID in
return splitOrder, nil return splitOrder, nil
} }
func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, items []entity.SplitBillItem) (*entity.Order, error) { func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Order, items []entity.SplitBillItem) (*entity.Order, error) {
var splitOrderItems []entity.OrderItem var splitOrderItems []entity.OrderItem
orderItemMap := make(map[int64]*entity.OrderItem) orderItemMap := make(map[int64]*entity.OrderItem)
for _, item := range originalOrder.OrderItems { for i := range originalOrder.OrderItems {
orderItemMap[item.ID] = &item orderItemMap[originalOrder.OrderItems[i].ID] = &originalOrder.OrderItems[i]
} }
assignedItems := make(map[int64]bool) assignedItems := make(map[int64]bool)
@ -275,7 +370,6 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
splitTotal := splitAmount + splitTax splitTotal := splitAmount + splitTax
// Create new PAID order for the split
splitOrder := &entity.Order{ splitOrder := &entity.Order{
PartnerID: originalOrder.PartnerID, PartnerID: originalOrder.PartnerID,
CustomerID: originalOrder.CustomerID, CustomerID: originalOrder.CustomerID,
@ -284,8 +378,6 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
Amount: splitAmount, Amount: splitAmount,
Tax: splitTax, Tax: splitTax,
Total: splitTotal, Total: splitTotal,
PaymentType: paymentMethod,
PaymentProvider: paymentProvider,
Source: originalOrder.Source, Source: originalOrder.Source,
CreatedBy: originalOrder.CreatedBy, CreatedBy: originalOrder.CreatedBy,
OrderItems: splitOrderItems, OrderItems: splitOrderItems,
@ -300,16 +392,13 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
return nil, err return nil, err
} }
// Adjust original order items (reduce quantities)
for _, item := range items { for _, item := range items {
orderItem := orderItemMap[item.OrderItemID] orderItem := orderItemMap[item.OrderItemID]
newQuantity := orderItem.Quantity - item.Quantity newQuantity := orderItem.Quantity - item.Quantity
if newQuantity == 0 { if newQuantity == 0 {
// Remove item completely
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0) err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0)
} else { } else {
// Update quantity
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, newQuantity) err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, newQuantity)
} }
@ -319,12 +408,10 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
} }
} }
// Recalculate original order totals
remainingAmount := originalOrder.Amount - splitAmount remainingAmount := originalOrder.Amount - splitAmount
remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax
remainingTotal := remainingAmount + remainingTax remainingTotal := remainingAmount + remainingTax
// Update original order totals
err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal) err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal)
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err)) logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err))
@ -335,7 +422,7 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
} }
// splitByAmount splits the order by assigning specific amounts to each split // splitByAmount splits the order by assigning specific amounts to each split
func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, amount float64) (*entity.Order, error) { func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, amount float64) (*entity.Order, error) {
// Validate that split amount is less than original order total // Validate that split amount is less than original order total
if amount >= originalOrder.Total { if amount >= originalOrder.Total {
return nil, errors.New(fmt.Sprintf("split amount %.2f must be less than order total %.2f", return nil, errors.New(fmt.Sprintf("split amount %.2f must be less than order total %.2f",
@ -362,7 +449,6 @@ func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Or
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
splitTotal := splitAmount + splitTax splitTotal := splitAmount + splitTax
// Create new PAID order for the split
splitOrder := &entity.Order{ splitOrder := &entity.Order{
PartnerID: originalOrder.PartnerID, PartnerID: originalOrder.PartnerID,
CustomerID: originalOrder.CustomerID, CustomerID: originalOrder.CustomerID,
@ -371,8 +457,6 @@ func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Or
Amount: splitAmount, Amount: splitAmount,
Tax: splitTax, Tax: splitTax,
Total: splitTotal, Total: splitTotal,
PaymentType: paymentMethod,
PaymentProvider: paymentProvider,
Source: originalOrder.Source, Source: originalOrder.Source,
CreatedBy: originalOrder.CreatedBy, CreatedBy: originalOrder.CreatedBy,
OrderItems: splitOrderItems, OrderItems: splitOrderItems,

View File

@ -15,24 +15,12 @@ type Repository interface {
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error
GetOrderPaymentMethodBreakdown( GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
ctx mycontext.Context, GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error)
partnerID int64, GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error)
req entity.SearchRequest, GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error)
) ([]entity.PaymentMethodBreakdown, error) GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, error)
GetRevenueOverview(
ctx mycontext.Context,
req entity.RevenueOverviewRequest,
) ([]entity.RevenueOverviewItem, error)
GetSalesByCategory(
ctx mycontext.Context,
req entity.SalesByCategoryRequest,
) ([]entity.SalesByCategoryItem, error)
GetPopularProducts(
ctx mycontext.Context,
req entity.PopularProductsRequest,
) ([]entity.PopularProductItem, error)
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error) FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error)
@ -71,8 +59,8 @@ type Service interface {
RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error
PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error
VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error)
GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) GetOrderHistory(ctx mycontext.Context, request entity.SearchRequest) ([]*entity.Order, int64, error)
CalculateOrderTotals( CalculateOrderTotals(
ctx mycontext.Context, ctx mycontext.Context,
items []entity.OrderItemRequest, items []entity.OrderItemRequest,
@ -110,6 +98,7 @@ type Service interface {
GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error) GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error)
GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error) GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error)
GetOrderByIDAndPartnerID(ctx mycontext.Context, orderID int64, partnerID int64) (*entity.Order, error)
} }
type Config interface { type Config interface {

View File

@ -4,12 +4,13 @@ import (
"enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/entity"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
) )
func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) { func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, request entity.SearchRequest) ([]*entity.Order, int64, error) {
return s.repo.GetOrderHistoryByPartnerID(ctx, partnerID, request) return s.repo.GetOrderHistoryByPartnerID(ctx, ctx.GetPartnerID(), request)
} }
func (s *orderSvc) GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) { func (s *orderSvc) GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) {
@ -39,3 +40,16 @@ func (s *orderSvc) GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.O
return order, nil return order, nil
} }
func (s *orderSvc) GetOrderByIDAndPartnerID(ctx mycontext.Context, orderID int64, partnerID int64) (*entity.Order, error) {
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get order by ID and partner ID",
zap.Error(err),
zap.Int64("orderID", orderID),
zap.Int64("partnerID", partnerID))
return nil, errors.Wrap(err, "failed to get order")
}
return order, nil
}

View File

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

View File

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