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
@@ -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 {
@@ -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)
}