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 {
ID int64
PartnerID int64
CashierID int64
OpenedAt time.Time
ClosedAt *time.Time

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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"
"enaklo-pos-be/internal/handlers/http/transaction"
"enaklo-pos-be/internal/handlers/http/user"
"net/http"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"net/http"
"enaklo-pos-be/internal/middlewares"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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