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
+2 -2
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{
+2 -2
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{
+2 -2
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 {
+1 -1
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),
+2 -2
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,
@@ -4,6 +4,7 @@ import (
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
"github.com/pkg/errors"
"go.uber.org/zap"
)
@@ -13,6 +14,7 @@ type Service interface {
CloseSession(ctx mycontext.Context, sessionID int64, closingAmount float64) (*entity.CashierSessionReport, error)
GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
GetSessionReport(ctx mycontext.Context, sessionID int64) (*entity.CashierSessionReport, error)
GetSessionHistory(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error)
}
type Repository interface {
@@ -21,6 +23,7 @@ type Repository interface {
GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error)
GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error)
GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error)
GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error)
}
type cashierSessionSvc struct {
@@ -97,3 +100,11 @@ func (s *cashierSessionSvc) GetSessionReport(ctx mycontext.Context, sessionID in
Payments: report,
}, nil
}
func (s *cashierSessionSvc) GetSessionHistory(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) {
sessions, total, err := s.repo.GetSessionHistoryByPartnerID(ctx, partnerID, limit, offset)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to get session history")
}
return sessions, total, nil
}
@@ -1,11 +1,17 @@
package inprogress_order
import (
"context"
"database/sql"
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext"
order2 "enaklo-pos-be/internal/constants/order"
"enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/services/v2/order"
"fmt"
"gorm.io/gorm"
"github.com/pkg/errors"
"go.uber.org/zap"
)
@@ -19,56 +25,185 @@ type InProgressOrderService interface {
type OrderRepository interface {
FindByID(ctx mycontext.Context, id int64) (*entity.Order, error)
CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error)
CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error)
CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error)
UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error
}
type OrderCalculator interface {
CalculateOrderTotals(
ctx mycontext.Context,
items []entity.OrderItemRequest,
productDetails *entity.ProductDetails,
source string,
partnerID int64,
) (*entity.OrderCalculation, error)
CalculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, partnerID int64) (*entity.OrderCalculation, error)
ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error)
}
type TransactionManager interface {
Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error)
Commit(session *gorm.DB) *gorm.DB
Rollback(session *gorm.DB) *gorm.DB
}
type inProgressOrderSvc struct {
repo OrderRepository
orderCalculator OrderCalculator
product order.ProductService
trx TransactionManager
}
func NewInProgressOrderService(repo OrderRepository, calculator OrderCalculator, product order.ProductService) InProgressOrderService {
func NewInProgressOrderService(repo OrderRepository,
calculator OrderCalculator, product order.ProductService, trx TransactionManager) InProgressOrderService {
return &inProgressOrderSvc{
repo: repo,
orderCalculator: calculator,
product: product,
trx: trx,
}
}
func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) {
productIDs, filteredItems, err := s.orderCalculator.ValidateOrderItems(ctx, req.OrderItems)
if err != nil {
return nil, err
}
req.OrderItems = filteredItems
productDetails, err := s.product.GetProductDetails(ctx, productIDs, req.PartnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err))
return nil, err
}
orderCalculation, err := s.orderCalculator.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source, req.PartnerID)
orderItems, err := s.prepareOrderItems(ctx, req.OrderItems, req.PartnerID)
if err != nil {
return nil, err
}
orderItems := make([]entity.OrderItem, len(req.OrderItems))
for i, item := range req.OrderItems {
orderCalculation, err := s.calculateOrderTotals(ctx, req.OrderItems, req.Source, req.PartnerID)
if err != nil {
return nil, err
}
tx, err := s.trx.Begin(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to begin transaction")
}
defer func() {
if r := recover(); r != nil {
s.trx.Rollback(tx)
}
}()
orderToSave := s.createOrderEntity(req, nil, orderCalculation) // Save order without items first
createdOrder, err := s.repo.CreateOrder(ctx, orderToSave, tx)
if err != nil {
s.trx.Rollback(tx)
if logger.ContextLogger(ctx) != nil {
logger.ContextLogger(ctx).Error("failed to create in-progress order", zap.Error(err), zap.Int64("partnerID", orderToSave.PartnerID))
}
return nil, errors.Wrap(err, "failed to create in-progress order")
}
err = s.repo.CreateOrderItems(ctx, createdOrder.ID, orderItems, tx)
if err != nil {
s.trx.Rollback(tx)
if logger.ContextLogger(ctx) != nil {
logger.ContextLogger(ctx).Error("failed to create order items", zap.Error(err), zap.Int64("orderID", createdOrder.ID))
}
return nil, errors.Wrap(err, "failed to create order items")
}
if err := s.trx.Commit(tx).Error; err != nil {
return nil, errors.Wrap(err, "failed to commit transaction")
}
fullOrder, err := s.repo.FindByID(ctx, createdOrder.ID)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch created order")
}
return fullOrder, nil
}
func (s *inProgressOrderSvc) AddItems(ctx mycontext.Context, orderID int64, newItems []entity.OrderItemRequest) (*entity.Order, error) {
existingOrder, err := s.repo.FindByID(ctx, orderID)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch order %d", orderID)
}
if existingOrder.Status != order2.Pending.String() {
return nil, errors.Errorf("cannot add items to order with status %s", existingOrder.Status)
}
newOrderItems, err := s.prepareOrderItems(ctx, newItems, existingOrder.PartnerID)
if err != nil {
return nil, err
}
tx, err := s.trx.Begin(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to begin transaction")
}
defer func() {
if r := recover(); r != nil {
s.trx.Rollback(tx)
}
}()
err = s.repo.CreateOrderItems(ctx, existingOrder.ID, newOrderItems, tx)
if err != nil {
s.trx.Rollback(tx)
if logger.ContextLogger(ctx) != nil {
logger.ContextLogger(ctx).Error("failed to add order items",
zap.Error(err),
zap.Int64("orderID", existingOrder.ID))
}
return nil, errors.Wrap(err, "failed to add order items")
}
updatedOrder, err := s.repo.FindByID(ctx, existingOrder.ID)
if err != nil {
s.trx.Rollback(tx)
return nil, errors.Wrap(err, "failed to fetch updated order")
}
combinedItemRequests := s.convertToOrderItemRequests(updatedOrder.OrderItems)
orderCalculation, err := s.calculateOrderTotals(ctx, combinedItemRequests, updatedOrder.Source, updatedOrder.PartnerID)
if err != nil {
s.trx.Rollback(tx)
return nil, err
}
updatedOrder.Total = orderCalculation.Total
updatedOrder.Tax = orderCalculation.Tax
updatedOrder.Amount = orderCalculation.Subtotal
err = s.repo.UpdateOrderTotalsWithTx(ctx,
tx,
updatedOrder.ID,
orderCalculation.Subtotal,
orderCalculation.Tax,
orderCalculation.Total)
if err != nil {
s.trx.Rollback(tx)
if logger.ContextLogger(ctx) != nil {
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err), zap.Int64("orderID", updatedOrder.ID))
}
return nil, errors.Wrap(err, "failed to update order totals")
}
if err := s.trx.Commit(tx).Error; err != nil {
return nil, errors.Wrap(err, "failed to commit transaction")
}
updatedOrder.OrderItems = newOrderItems
return updatedOrder, nil
}
func (s *inProgressOrderSvc) prepareOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest, partnerID int64) ([]entity.OrderItem, error) {
productIDs, filteredItems, err := s.orderCalculator.ValidateOrderItems(ctx, items)
if err != nil {
return nil, err
}
productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID)
if err != nil {
if logger.ContextLogger(ctx) != nil {
logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err))
}
return nil, err
}
orderItems := make([]entity.OrderItem, len(filteredItems))
for i, item := range filteredItems {
product, exists := productDetails.Products[item.ProductID]
productName := ""
if exists {
@@ -76,17 +211,34 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques
}
orderItems[i] = entity.OrderItem{
ItemID: item.ProductID,
ItemName: productName,
Quantity: item.Quantity,
Price: product.Price,
ItemType: product.Type,
Description: product.Description,
Notes: item.Notes,
ItemID: item.ProductID,
ItemName: productName,
Quantity: item.Quantity,
Price: product.Price,
ItemType: product.Type,
Notes: item.Notes,
}
}
order := &entity.Order{
return orderItems, nil
}
func (s *inProgressOrderSvc) calculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, source string, partnerID int64) (*entity.OrderCalculation, error) {
productIDs, _, err := s.orderCalculator.ValidateOrderItems(ctx, items)
if err != nil {
return nil, err
}
productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID)
if err != nil {
return nil, err
}
return s.orderCalculator.CalculateOrderTotals(ctx, items, productDetails, source, partnerID)
}
func (s *inProgressOrderSvc) createOrderEntity(req *entity.OrderRequest, orderItems []entity.OrderItem, calculation *entity.OrderCalculation) *entity.Order {
return &entity.Order{
ID: req.ID,
PartnerID: req.PartnerID,
CustomerID: req.CustomerID,
@@ -95,22 +247,50 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderReques
OrderItems: orderItems,
TableNumber: req.TableNumber,
OrderType: req.OrderType,
Total: orderCalculation.Total,
Tax: orderCalculation.Tax,
Amount: orderCalculation.Subtotal,
Total: calculation.Total,
Tax: calculation.Tax,
Amount: calculation.Subtotal,
Status: order2.Pending.String(),
Source: req.Source,
}
}
createdOrder, err := s.repo.CreateOrUpdate(ctx, order)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create in-progress order",
zap.Error(err),
zap.Int64("partnerID", order.PartnerID))
return nil, errors.Wrap(err, "failed to create in-progress order")
func (s *inProgressOrderSvc) convertToOrderItemRequests(items []entity.OrderItem) []entity.OrderItemRequest {
requests := make([]entity.OrderItemRequest, len(items))
for i, item := range items {
requests[i] = entity.OrderItemRequest{
ProductID: item.ItemID,
Quantity: item.Quantity,
Notes: item.Notes,
}
}
return requests
}
func (s *inProgressOrderSvc) extractNewlyAddedItems(updatedOrder *entity.Order, existingItems []entity.OrderItem) []entity.OrderItem {
if len(existingItems) == 0 {
return updatedOrder.OrderItems
}
return createdOrder, nil
existingItemMap := make(map[string]struct{})
for _, item := range existingItems {
key := s.createItemKey(item)
existingItemMap[key] = struct{}{}
}
newlyAdded := make([]entity.OrderItem, 0)
for _, item := range updatedOrder.OrderItems {
key := s.createItemKey(item)
if _, exists := existingItemMap[key]; !exists {
newlyAdded = append(newlyAdded, item)
}
}
return newlyAdded
}
func (s *inProgressOrderSvc) createItemKey(item entity.OrderItem) string {
return fmt.Sprintf("%d_%s", item.ItemID, item.Notes)
}
func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) {
@@ -127,79 +307,6 @@ func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partner
return orders, nil
}
func (s *inProgressOrderSvc) AddItems(ctx mycontext.Context, orderID int64, newItems []entity.OrderItemRequest) (*entity.Order, error) {
existingOrder, err := s.repo.FindByID(ctx, orderID)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch order %d", orderID)
}
type itemKey struct {
ProductID int64
Notes string
}
itemMap := make(map[itemKey]entity.OrderItemRequest)
existingKeys := make(map[itemKey]struct{})
// Collect existing items
for _, oi := range existingOrder.OrderItems {
key := itemKey{ProductID: oi.ItemID, Notes: oi.Notes}
existingKeys[key] = struct{}{}
itemMap[key] = entity.OrderItemRequest{
ProductID: oi.ItemID,
Quantity: oi.Quantity,
Notes: oi.Notes,
}
}
// Merge new items into map
for _, ni := range newItems {
key := itemKey{ProductID: ni.ProductID, Notes: ni.Notes}
if existing, found := itemMap[key]; found {
existing.Quantity += ni.Quantity
itemMap[key] = existing
} else {
itemMap[key] = ni
}
}
// Prepare merged items
mergedItems := make([]entity.OrderItemRequest, 0, len(itemMap))
for _, item := range itemMap {
mergedItems = append(mergedItems, item)
}
// Save updated order
req := &entity.OrderRequest{
ID: existingOrder.ID,
PartnerID: existingOrder.PartnerID,
CustomerID: existingOrder.CustomerID,
CustomerName: existingOrder.CustomerName,
CreatedBy: existingOrder.CreatedBy,
TableNumber: existingOrder.TableNumber,
OrderType: existingOrder.OrderType,
Source: existingOrder.Source,
OrderItems: mergedItems,
}
savedOrder, err := s.Save(ctx, req)
if err != nil {
return nil, err
}
newlyAdded := make([]entity.OrderItem, 0)
for _, item := range savedOrder.OrderItems {
key := itemKey{ProductID: item.ItemID, Notes: item.Notes}
if _, exists := existingKeys[key]; !exists {
newlyAdded = append(newlyAdded, item)
}
}
savedOrder.OrderItems = newlyAdded
return savedOrder, nil
}
func (s *inProgressOrderSvc) GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) {
orders, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
if err != nil {
@@ -0,0 +1,898 @@
package inprogress_order
import (
"context"
"database/sql"
"enaklo-pos-be/internal/common/mycontext"
order2 "enaklo-pos-be/internal/constants/order"
"enaklo-pos-be/internal/entity"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"gorm.io/gorm"
)
// Mock implementations
type MockOrderRepository struct {
mock.Mock
}
func (m *MockOrderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.Order), args.Error(1)
}
func (m *MockOrderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) {
args := m.Called(ctx, order, tx)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.Order), args.Error(1)
}
func (m *MockOrderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error {
args := m.Called(ctx, orderID, items, tx)
return args.Error(0)
}
func (m *MockOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) {
args := m.Called(ctx, partnerID, limit, offset, status)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*entity.Order), args.Error(1)
}
func (m *MockOrderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) {
args := m.Called(ctx, id, partnerID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.Order), args.Error(1)
}
type MockOrderCalculator struct {
mock.Mock
}
func (m *MockOrderCalculator) CalculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, partnerID int64) (*entity.OrderCalculation, error) {
args := m.Called(ctx, items, productDetails, source, partnerID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.OrderCalculation), args.Error(1)
}
func (m *MockOrderCalculator) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) {
args := m.Called(ctx, items)
// Handle nil values properly
var productIDs []int64
if args.Get(0) != nil {
productIDs = args.Get(0).([]int64)
}
var filteredItems []entity.OrderItemRequest
if args.Get(1) != nil {
filteredItems = args.Get(1).([]entity.OrderItemRequest)
}
return productIDs, filteredItems, args.Error(2)
}
type MockProductService struct {
mock.Mock
}
func (m *MockProductService) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) {
args := m.Called(ctx, productIDs, partnerID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.ProductDetails), args.Error(1)
}
func (m *MockProductService) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) {
args := m.Called(ctx, ids, partnerID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*entity.Product), args.Error(1)
}
type MockTransactionManager struct {
mock.Mock
}
func (m *MockTransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) {
args := m.Called(ctx, opts)
return args.Get(0).(*gorm.DB), args.Error(1)
}
func (m *MockTransactionManager) Commit(session *gorm.DB) *gorm.DB {
args := m.Called(session)
return args.Get(0).(*gorm.DB)
}
func (m *MockTransactionManager) Rollback(session *gorm.DB) *gorm.DB {
args := m.Called(session)
return args.Get(0).(*gorm.DB)
}
func TestInProgressOrderService_Save(t *testing.T) {
tests := []struct {
name string
request *entity.OrderRequest
setupMocks func(*MockOrderRepository, *MockOrderCalculator, *MockProductService)
expectedResult *entity.Order
expectedError string
}{
{
name: "successful order creation",
request: &entity.OrderRequest{
ID: 1,
PartnerID: 100,
CustomerID: func() *int64 { id := int64(200); return &id }(),
CustomerName: "John Doe",
CreatedBy: 300,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 2, Notes: "Extra spicy"},
{ProductID: 2, Quantity: 1, Notes: ""},
},
TableNumber: "A1",
OrderType: "DINE_IN",
Source: "POS",
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
// Mock ValidateOrderItems
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1, 2},
[]entity.OrderItemRequest{
{ProductID: 1, Quantity: 2, Notes: "Extra spicy"},
{ProductID: 2, Quantity: 1, Notes: ""},
},
nil,
)
// Mock GetProductDetails
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
2: {ID: 2, Name: "Fries", Price: 5.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1, 2}, int64(100)).Return(productDetails, nil)
// Mock CalculateOrderTotals
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "POS", int64(100)).Return(
&entity.OrderCalculation{
Subtotal: 25.0,
Tax: 2.5,
Total: 27.5,
},
nil,
)
// Mock CreateOrder (returns order without items)
createdOrder := &entity.Order{
ID: 1,
PartnerID: 100,
CustomerID: func() *int64 { id := int64(200); return &id }(),
CustomerName: "John Doe",
CreatedBy: 300,
TableNumber: "A1",
OrderType: "DINE_IN",
Total: 27.5,
Tax: 2.5,
Amount: 25.0,
Status: order2.Pending.String(),
Source: "POS",
}
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(createdOrder, nil)
// Mock CreateOrderItems
repo.On("CreateOrderItems", mock.Anything, int64(1), mock.Anything).Return(nil)
// Mock FindByID (returns full order with items)
expectedOrder := &entity.Order{
ID: 1,
PartnerID: 100,
CustomerID: func() *int64 { id := int64(200); return &id }(),
CustomerName: "John Doe",
CreatedBy: 300,
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
{ItemID: 2, ItemName: "Fries", Quantity: 1, Price: 5.0, ItemType: "PRODUCT", Notes: ""},
},
TableNumber: "A1",
OrderType: "DINE_IN",
Total: 27.5,
Tax: 2.5,
Amount: 25.0,
Status: order2.Pending.String(),
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(expectedOrder, nil)
},
expectedResult: &entity.Order{
ID: 1,
PartnerID: 100,
CustomerID: func() *int64 { id := int64(200); return &id }(),
CustomerName: "John Doe",
CreatedBy: 300,
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
{ItemID: 2, ItemName: "Fries", Quantity: 1, Price: 5.0, ItemType: "PRODUCT", Notes: ""},
},
TableNumber: "A1",
OrderType: "DINE_IN",
Total: 27.5,
Tax: 2.5,
Amount: 25.0,
Status: order2.Pending.String(),
Source: "POS",
},
expectedError: "",
},
{
name: "validation error",
request: &entity.OrderRequest{
PartnerID: 100,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 0}, // Invalid quantity
},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
nil, nil, errors.New("invalid quantity"),
)
},
expectedResult: nil,
expectedError: "invalid quantity",
},
{
name: "product details error",
request: &entity.OrderRequest{
PartnerID: 100,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(nil, errors.New("product not found"))
},
expectedResult: nil,
expectedError: "product not found",
},
{
name: "calculation error",
request: &entity.OrderRequest{
PartnerID: 100,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "", int64(100)).Return(
nil, errors.New("calculation failed"),
)
},
expectedResult: nil,
expectedError: "calculation failed",
},
{
name: "repository error",
request: &entity.OrderRequest{
PartnerID: 100,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "", int64(100)).Return(
&entity.OrderCalculation{Subtotal: 10.0, Tax: 1.0, Total: 11.0},
nil,
)
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(nil, errors.New("database error"))
},
expectedResult: nil,
expectedError: "failed to create in-progress order: database error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mocks
mockRepo := &MockOrderRepository{}
mockCalc := &MockOrderCalculator{}
mockProd := &MockProductService{}
if tt.setupMocks != nil {
tt.setupMocks(mockRepo, mockCalc, mockProd)
}
// Create service
service := NewInProgressOrderService(mockRepo, mockCalc, mockProd)
// Execute
ctx := mycontext.NewContext(context.Background())
result, err := service.Save(ctx, tt.request)
// Assert
if tt.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.expectedResult.ID, result.ID)
assert.Equal(t, tt.expectedResult.PartnerID, result.PartnerID)
assert.Equal(t, tt.expectedResult.Status, result.Status)
assert.Equal(t, tt.expectedResult.Total, result.Total)
assert.Len(t, result.OrderItems, len(tt.expectedResult.OrderItems))
}
// Verify all mocks were called as expected
mockRepo.AssertExpectations(t)
mockCalc.AssertExpectations(t)
mockProd.AssertExpectations(t)
})
}
}
func TestInProgressOrderService_AddItems(t *testing.T) {
tests := []struct {
name string
orderID int64
newItems []entity.OrderItemRequest
setupMocks func(*MockOrderRepository, *MockOrderCalculator, *MockProductService)
expectedResult *entity.Order
expectedError string
}{
{
name: "successful add items to pending order",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 3, Quantity: 1, Notes: "No onions"},
{ProductID: 4, Quantity: 2, Notes: ""},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
// Mock existing order
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil).Once()
// Mock ValidateOrderItems for new items
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{3, 4},
[]entity.OrderItemRequest{
{ProductID: 3, Quantity: 1, Notes: "No onions"},
{ProductID: 4, Quantity: 2, Notes: ""},
},
nil,
)
// Mock GetProductDetails
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
3: {ID: 3, Name: "Salad", Price: 8.0, Type: "PRODUCT"},
4: {ID: 4, Name: "Drink", Price: 3.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{3, 4}, int64(100)).Return(productDetails, nil)
// Mock CreateOrderItems
repo.On("CreateOrderItems", mock.Anything, int64(1), mock.Anything).Return(nil)
// Mock FindByID (returns updated order with all items)
updatedOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
{ItemID: 3, ItemName: "Salad", Quantity: 1, Price: 8.0, ItemType: "PRODUCT", Notes: "No onions"},
{ItemID: 4, ItemName: "Drink", Quantity: 2, Price: 3.0, ItemType: "PRODUCT", Notes: ""},
},
Total: 40.7,
Tax: 3.7,
Amount: 37.0,
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(updatedOrder, nil).Once()
// Mock CalculateOrderTotals for combined items
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
&entity.OrderCalculation{
Subtotal: 37.0,
Tax: 3.7,
Total: 40.7,
},
nil,
)
// Mock CreateOrder for updating totals
repo.On("CreateOrder", mock.Anything, mock.Anything).Return(updatedOrder, nil)
},
expectedResult: &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 3, ItemName: "Salad", Quantity: 1, Price: 8.0, ItemType: "PRODUCT", Notes: "No onions"},
{ItemID: 4, ItemName: "Drink", Quantity: 2, Price: 3.0, ItemType: "PRODUCT", Notes: ""},
},
Total: 40.7,
Tax: 3.7,
Amount: 37.0,
Source: "POS",
},
expectedError: "",
},
{
name: "order not found",
orderID: 999,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
repo.On("FindByID", mock.Anything, int64(999)).Return(nil, errors.New("order not found"))
},
expectedResult: nil,
expectedError: "failed to fetch order 999: order not found",
},
{
name: "order not in pending status",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
Status: order2.Paid.String(),
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
},
expectedResult: nil,
expectedError: "cannot add items to order with status PAID",
},
{
name: "validation error for new items",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 0}, // Invalid quantity
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
nil, nil, errors.New("invalid quantity"),
)
},
expectedResult: nil,
expectedError: "invalid quantity",
},
{
name: "product details error",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(nil, errors.New("product not found"))
},
expectedResult: nil,
expectedError: "product not found",
},
{
name: "calculation error",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
nil, errors.New("calculation failed"),
)
},
expectedResult: nil,
expectedError: "calculation failed",
},
{
name: "repository update error",
orderID: 1,
newItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 1},
},
setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) {
existingOrder := &entity.Order{
ID: 1,
PartnerID: 100,
Status: order2.Pending.String(),
OrderItems: []entity.OrderItem{
{ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"},
},
Source: "POS",
}
repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil)
calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return(
[]int64{1},
[]entity.OrderItemRequest{{ProductID: 1, Quantity: 1}},
nil,
)
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"},
},
}
prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil)
calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return(
&entity.OrderCalculation{Subtotal: 30.0, Tax: 3.0, Total: 33.0},
nil,
)
repo.On("CreateOrderItems", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("database error"))
},
expectedResult: nil,
expectedError: "failed to add order items: database error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mocks
mockRepo := &MockOrderRepository{}
mockCalc := &MockOrderCalculator{}
mockProd := &MockProductService{}
if tt.setupMocks != nil {
tt.setupMocks(mockRepo, mockCalc, mockProd)
}
// Create service
service := NewInProgressOrderService(mockRepo, mockCalc, mockProd)
// Execute
ctx := mycontext.NewContext(context.Background())
result, err := service.AddItems(ctx, tt.orderID, tt.newItems)
// Assert
if tt.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.expectedResult.ID, result.ID)
assert.Equal(t, tt.expectedResult.PartnerID, result.PartnerID)
assert.Equal(t, tt.expectedResult.Status, result.Status)
assert.Equal(t, tt.expectedResult.Total, result.Total)
// Should only return newly added items
assert.Len(t, result.OrderItems, len(tt.expectedResult.OrderItems))
}
// Verify all mocks were called as expected
mockRepo.AssertExpectations(t)
mockCalc.AssertExpectations(t)
mockProd.AssertExpectations(t)
})
}
}
func TestInProgressOrderService_HelperMethods(t *testing.T) {
service := &inProgressOrderSvc{}
t.Run("convertToOrderItemRequests", func(t *testing.T) {
items := []entity.OrderItem{
{ItemID: 1, Quantity: 2, Notes: "Extra spicy"},
{ItemID: 2, Quantity: 1, Notes: ""},
}
result := service.convertToOrderItemRequests(items)
assert.Len(t, result, 2)
assert.Equal(t, int64(1), result[0].ProductID)
assert.Equal(t, 2, result[0].Quantity)
assert.Equal(t, "Extra spicy", result[0].Notes)
assert.Equal(t, int64(2), result[1].ProductID)
assert.Equal(t, 1, result[1].Quantity)
assert.Equal(t, "", result[1].Notes)
})
t.Run("createItemKey", func(t *testing.T) {
item := entity.OrderItem{ItemID: 1, Notes: "Extra spicy"}
key := service.createItemKey(item)
assert.Equal(t, "1_Extra spicy", key)
item2 := entity.OrderItem{ItemID: 2, Notes: ""}
key2 := service.createItemKey(item2)
assert.Equal(t, "2_", key2)
})
t.Run("extractNewlyAddedItems", func(t *testing.T) {
existingItems := []entity.OrderItem{
{ItemID: 1, Notes: "Extra spicy"},
{ItemID: 2, Notes: ""},
}
updatedOrder := &entity.Order{
OrderItems: []entity.OrderItem{
{ItemID: 1, Notes: "Extra spicy"},
{ItemID: 2, Notes: ""},
{ItemID: 3, Notes: "No onions"},
{ItemID: 4, Notes: ""},
},
}
result := service.extractNewlyAddedItems(updatedOrder, existingItems)
assert.Len(t, result, 2)
assert.Equal(t, int64(3), result[0].ItemID)
assert.Equal(t, "No onions", result[0].Notes)
assert.Equal(t, int64(4), result[1].ItemID)
assert.Equal(t, "", result[1].Notes)
})
t.Run("extractNewlyAddedItems with no existing items", func(t *testing.T) {
updatedOrder := &entity.Order{
OrderItems: []entity.OrderItem{
{ItemID: 1, Notes: "Extra spicy"},
{ItemID: 2, Notes: ""},
},
}
result := service.extractNewlyAddedItems(updatedOrder, []entity.OrderItem{})
assert.Len(t, result, 2)
assert.Equal(t, int64(1), result[0].ItemID)
assert.Equal(t, int64(2), result[1].ItemID)
})
}
func TestSave_WithTransaction(t *testing.T) {
// Setup
mockRepo := new(MockOrderRepository)
mockCalculator := new(MockOrderCalculator)
mockProduct := new(MockProductService)
mockTrx := new(MockTransactionManager)
service := NewInProgressOrderService(mockRepo, mockCalculator, mockProduct, mockTrx)
ctx := mycontext.NewContext(context.Background())
req := &entity.OrderRequest{
PartnerID: 1,
OrderItems: []entity.OrderItemRequest{
{ProductID: 1, Quantity: 2},
},
Source: "pos",
}
// Mock transaction
mockTx := &gorm.DB{}
mockTrx.On("Begin", ctx, mock.Anything).Return(mockTx, nil)
mockTrx.On("Commit", mockTx).Return(mockTx)
mockTrx.On("Rollback", mockTx).Return(mockTx)
// Mock calculator
productIDs := []int64{1}
filteredItems := []entity.OrderItemRequest{{ProductID: 1, Quantity: 2}}
mockCalculator.On("ValidateOrderItems", ctx, req.OrderItems).Return(productIDs, filteredItems, nil)
// Mock product service
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
1: {ID: 1, Name: "Test Product", Price: 10.0},
},
}
mockProduct.On("GetProductDetails", ctx, productIDs, req.PartnerID).Return(productDetails, nil)
// Mock calculation
calculation := &entity.OrderCalculation{
Subtotal: 20.0,
Tax: 2.0,
Total: 22.0,
}
mockCalculator.On("CalculateOrderTotals", ctx, req.OrderItems, productDetails, req.Source, req.PartnerID).Return(calculation, nil)
// Mock repository calls
createdOrder := &entity.Order{ID: 1, PartnerID: 1}
mockRepo.On("CreateOrder", ctx, mock.AnythingOfType("*entity.Order"), mockTx).Return(createdOrder, nil)
mockRepo.On("CreateOrderItems", ctx, int64(1), mock.AnythingOfType("[]entity.OrderItem"), mockTx).Return(nil)
fullOrder := &entity.Order{ID: 1, PartnerID: 1, OrderItems: []entity.OrderItem{}}
mockRepo.On("FindByID", ctx, int64(1)).Return(fullOrder, nil)
// Execute
result, err := service.Save(ctx, req)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, int64(1), result.ID)
// Verify all mocks were called
mockTrx.AssertExpectations(t)
mockRepo.AssertExpectations(t)
mockCalculator.AssertExpectations(t)
mockProduct.AssertExpectations(t)
}
func TestAddItems_WithTransaction(t *testing.T) {
// Setup
mockRepo := new(MockOrderRepository)
mockCalculator := new(MockOrderCalculator)
mockProduct := new(MockProductService)
mockTrx := new(MockTransactionManager)
service := NewInProgressOrderService(mockRepo, mockCalculator, mockProduct, mockTrx)
ctx := mycontext.NewContext(context.Background())
orderID := int64(1)
newItems := []entity.OrderItemRequest{
{ProductID: 2, Quantity: 1},
}
// Mock existing order
existingOrder := &entity.Order{
ID: orderID,
Status: "pending",
OrderItems: []entity.OrderItem{
{ItemID: 1, Quantity: 2},
},
}
mockRepo.On("FindByID", ctx, orderID).Return(existingOrder, nil)
// Mock transaction
mockTx := &gorm.DB{}
mockTrx.On("Begin", ctx, mock.Anything).Return(mockTx, nil)
mockTrx.On("Commit", mockTx).Return(mockTx)
mockTrx.On("Rollback", mockTx).Return(mockTx)
// Mock calculator
productIDs := []int64{2}
filteredItems := []entity.OrderItemRequest{{ProductID: 2, Quantity: 1}}
mockCalculator.On("ValidateOrderItems", ctx, newItems).Return(productIDs, filteredItems, nil)
// Mock product service
productDetails := &entity.ProductDetails{
Products: map[int64]*entity.Product{
2: {ID: 2, Name: "New Product", Price: 15.0},
},
}
mockProduct.On("GetProductDetails", ctx, productIDs, existingOrder.PartnerID).Return(productDetails, nil)
// Mock repository calls
mockRepo.On("CreateOrderItems", ctx, orderID, mock.AnythingOfType("[]entity.OrderItem"), mockTx).Return(nil)
updatedOrder := &entity.Order{
ID: orderID,
Status: "pending",
OrderItems: []entity.OrderItem{
{ItemID: 1, Quantity: 2},
{ItemID: 2, Quantity: 1},
},
}
mockRepo.On("FindByID", ctx, orderID).Return(updatedOrder, nil)
// Mock calculation for updated totals
combinedItems := []entity.OrderItemRequest{
{ProductID: 1, Quantity: 2},
{ProductID: 2, Quantity: 1},
}
updatedCalculation := &entity.OrderCalculation{
Subtotal: 35.0,
Tax: 3.5,
Total: 38.5,
}
mockCalculator.On("CalculateOrderTotals", ctx, combinedItems, productDetails, updatedOrder.Source, updatedOrder.PartnerID).Return(updatedCalculation, nil)
updatedOrderWithTotals := &entity.Order{
ID: orderID,
Status: "pending",
Total: 38.5,
Tax: 3.5,
Amount: 35.0,
OrderItems: []entity.OrderItem{
{ItemID: 1, Quantity: 2},
{ItemID: 2, Quantity: 1},
},
}
mockRepo.On("CreateOrder", ctx, mock.AnythingOfType("*entity.Order"), mockTx).Return(updatedOrderWithTotals, nil)
// Execute
result, err := service.AddItems(ctx, orderID, newItems)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, orderID, result.ID)
// Verify all mocks were called
mockTrx.AssertExpectations(t)
mockRepo.AssertExpectations(t)
mockCalculator.AssertExpectations(t)
mockProduct.AssertExpectations(t)
}
@@ -5,6 +5,7 @@ import (
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
"fmt"
"github.com/pkg/errors"
"go.uber.org/zap"
)
@@ -108,25 +109,61 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
return err
}
// Only allow voiding for NEW, PENDING orders
if order.Status != "NEW" && order.Status != "PENDING" {
return errors.New("only new or pending orders can be voided")
}
if voidType == "ALL" {
// Void entire order
// Void all items - create new VOIDED items for all existing items
for _, orderItem := range order.OrderItems {
if orderItem.Status == "ACTIVE" && orderItem.Quantity > 0 {
// Create new VOIDED order item with the voided quantity
voidedItem := &entity.OrderItem{
OrderID: orderID,
ItemID: orderItem.ItemID,
ItemType: orderItem.ItemType,
Price: orderItem.Price,
Quantity: orderItem.Quantity, // Void the full quantity
Status: "VOIDED",
CreatedBy: orderItem.CreatedBy,
ItemName: orderItem.ItemName,
Notes: reason, // Use the reason as notes for tracking
}
err = s.repo.CreateOrderItem(ctx, orderID, voidedItem)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create voided order item", zap.Error(err))
return err
}
// Update original item quantity to 0
err = s.repo.UpdateOrderItem(ctx, orderItem.ID, 0)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update original order item", zap.Error(err))
return err
}
}
}
// Update order status to VOIDED
err = s.repo.UpdateOrder(ctx, orderID, "VOIDED", reason)
if err != nil {
logger.ContextLogger(ctx).Error("failed to void order", zap.Error(err))
return err
}
// Recalculate order totals (should be 0 for voided order)
err = s.repo.UpdateOrderTotals(ctx, orderID, 0, 0, 0)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
return err
}
} else if voidType == "ITEM" {
// Void specific items
voidedAmount := 0.0
orderItemMap := make(map[int64]*entity.OrderItem)
for _, item := range order.OrderItems {
orderItemMap[item.ID] = &item
for i := range order.OrderItems {
orderItemMap[order.OrderItems[i].ID] = &order.OrderItems[i]
}
for _, voidItem := range items {
@@ -135,55 +172,114 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
return errors.New(fmt.Sprintf("order item %d not found", voidItem.OrderItemID))
}
if orderItem.Status != "ACTIVE" {
return errors.New(fmt.Sprintf("order item %d is not active", voidItem.OrderItemID))
}
if voidItem.Quantity > orderItem.Quantity {
return errors.New(fmt.Sprintf("void quantity %d exceeds available quantity %d for item %d",
voidItem.Quantity, orderItem.Quantity, voidItem.OrderItemID))
}
voidedAmount += orderItem.Price * float64(voidItem.Quantity)
}
// Update order items with reduced quantities
for _, voidItem := range items {
orderItem := orderItemMap[voidItem.OrderItemID]
newQuantity := orderItem.Quantity - voidItem.Quantity
if newQuantity == 0 {
// Remove item completely
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, 0)
} else {
// Update quantity
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity)
// Create new VOIDED order item with the voided quantity
voidedItem := &entity.OrderItem{
OrderID: orderID,
ItemID: orderItem.ItemID,
ItemType: orderItem.ItemType,
Price: orderItem.Price,
Quantity: voidItem.Quantity, // Void the requested quantity
Status: "VOIDED",
CreatedBy: orderItem.CreatedBy,
ItemName: orderItem.ItemName,
Notes: reason, // Use the reason as notes for tracking
}
err = s.repo.CreateOrderItem(ctx, orderID, voidedItem)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create voided order item", zap.Error(err))
return err
}
// Update original item quantity
newQuantity := orderItem.Quantity - voidItem.Quantity
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err))
return err
}
}
// Recalculate order totals
remainingAmount := order.Amount - voidedAmount
remainingTax := (remainingAmount / order.Amount) * order.Tax
remainingTotal := remainingAmount + remainingTax
// Update order totals
err = s.repo.UpdateOrderTotals(ctx, orderID, remainingAmount, remainingTax, remainingTotal)
updatedOrder, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
logger.ContextLogger(ctx).Error("failed to fetch updated order for recalculation", zap.Error(err))
return err
}
// Update order status to PARTIAL if some items remain, otherwise to VOIDED
newStatus := "PARTIAL"
if remainingAmount <= 0 {
newStatus = "VOIDED"
var activeItems []entity.OrderItemRequest
for _, item := range updatedOrder.OrderItems {
if item.Status == "ACTIVE" && item.Quantity > 0 {
activeItems = append(activeItems, entity.OrderItemRequest{
ProductID: item.ItemID,
Quantity: item.Quantity,
Notes: item.Notes,
})
}
}
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
return err
if len(activeItems) > 0 {
productIDs, _, err := s.ValidateOrderItems(ctx, activeItems)
if err != nil {
logger.ContextLogger(ctx).Error("failed to validate order items for recalculation", zap.Error(err))
return err
}
productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get product details for recalculation", zap.Error(err))
return err
}
orderCalculation, err := s.CalculateOrderTotals(ctx, activeItems, productDetails, order.Source, partnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to calculate order totals", zap.Error(err))
return err
}
// Update order totals
err = s.repo.UpdateOrderTotals(ctx, orderID, orderCalculation.Subtotal, orderCalculation.Tax, orderCalculation.Total)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
return err
}
// Update order status based on remaining amount
newStatus := "PENDING"
if orderCalculation.Subtotal <= 0 {
newStatus = "CANCELED"
}
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
return err
}
} else {
// No active items left, cancel the order
err = s.repo.UpdateOrderTotals(ctx, orderID, 0, 0, 0)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
return err
}
err = s.repo.UpdateOrder(ctx, orderID, "CANCELED", reason)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
return err
}
}
}
@@ -195,8 +291,7 @@ func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID in
return nil
}
// SplitBillRequest handles splitting bills by items or amounts
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to find order for split bill", zap.Error(err))
@@ -210,9 +305,9 @@ func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID in
var splitOrder *entity.Order
if splitType == "ITEM" {
splitOrder, err = s.splitByItems(ctx, order, paymentMethod, paymentProvider, items)
splitOrder, err = s.splitByItems(ctx, order, items)
} else if splitType == "AMOUNT" {
splitOrder, err = s.splitByAmount(ctx, order, paymentMethod, paymentProvider, amount)
splitOrder, err = s.splitByAmount(ctx, order, amount)
}
if err != nil {
@@ -228,12 +323,12 @@ func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID in
return splitOrder, nil
}
func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, items []entity.SplitBillItem) (*entity.Order, error) {
func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Order, items []entity.SplitBillItem) (*entity.Order, error) {
var splitOrderItems []entity.OrderItem
orderItemMap := make(map[int64]*entity.OrderItem)
for _, item := range originalOrder.OrderItems {
orderItemMap[item.ID] = &item
for i := range originalOrder.OrderItems {
orderItemMap[originalOrder.OrderItems[i].ID] = &originalOrder.OrderItems[i]
}
assignedItems := make(map[int64]bool)
@@ -275,7 +370,6 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
splitTotal := splitAmount + splitTax
// Create new PAID order for the split
splitOrder := &entity.Order{
PartnerID: originalOrder.PartnerID,
CustomerID: originalOrder.CustomerID,
@@ -284,8 +378,6 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
Amount: splitAmount,
Tax: splitTax,
Total: splitTotal,
PaymentType: paymentMethod,
PaymentProvider: paymentProvider,
Source: originalOrder.Source,
CreatedBy: originalOrder.CreatedBy,
OrderItems: splitOrderItems,
@@ -300,16 +392,13 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
return nil, err
}
// Adjust original order items (reduce quantities)
for _, item := range items {
orderItem := orderItemMap[item.OrderItemID]
newQuantity := orderItem.Quantity - item.Quantity
if newQuantity == 0 {
// Remove item completely
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0)
} else {
// Update quantity
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, newQuantity)
}
@@ -319,12 +408,10 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
}
}
// Recalculate original order totals
remainingAmount := originalOrder.Amount - splitAmount
remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax
remainingTotal := remainingAmount + remainingTax
// Update original order totals
err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err))
@@ -335,7 +422,7 @@ func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Ord
}
// splitByAmount splits the order by assigning specific amounts to each split
func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, amount float64) (*entity.Order, error) {
func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, amount float64) (*entity.Order, error) {
// Validate that split amount is less than original order total
if amount >= originalOrder.Total {
return nil, errors.New(fmt.Sprintf("split amount %.2f must be less than order total %.2f",
@@ -362,7 +449,6 @@ func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Or
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
splitTotal := splitAmount + splitTax
// Create new PAID order for the split
splitOrder := &entity.Order{
PartnerID: originalOrder.PartnerID,
CustomerID: originalOrder.CustomerID,
@@ -371,8 +457,6 @@ func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Or
Amount: splitAmount,
Tax: splitTax,
Total: splitTotal,
PaymentType: paymentMethod,
PaymentProvider: paymentProvider,
Source: originalOrder.Source,
CreatedBy: originalOrder.CreatedBy,
OrderItems: splitOrderItems,
+9 -20
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 {
+16 -2
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
}