add product ingredients

This commit is contained in:
Aditya Siregar
2025-09-12 15:37:19 +07:00
parent efe09c21e4
commit 3a04990ec8
35 changed files with 2572 additions and 357 deletions
+193 -21
View File
@@ -1,13 +1,17 @@
package service
import (
"apskel-pos-be/internal/appcontext"
"context"
"fmt"
"time"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/repository"
"apskel-pos-be/internal/util"
"github.com/google/uuid"
)
@@ -27,14 +31,22 @@ type OrderService interface {
}
type OrderServiceImpl struct {
orderProcessor processor.OrderProcessor
tableRepo repository.TableRepositoryInterface
orderProcessor processor.OrderProcessor
tableRepo repository.TableRepositoryInterface
orderIngredientTransactionService *OrderIngredientTransactionService
orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor
productIngredientRepo repository.ProductIngredientRepository
txManager *repository.TxManager
}
func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface) *OrderServiceImpl {
func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productIngredientRepo repository.ProductIngredientRepository, txManager *repository.TxManager) *OrderServiceImpl {
return &OrderServiceImpl{
orderProcessor: orderProcessor,
tableRepo: tableRepo,
orderProcessor: orderProcessor,
tableRepo: tableRepo,
orderIngredientTransactionService: orderIngredientTransactionService,
orderIngredientTransactionProcessor: orderIngredientTransactionProcessor,
productIngredientRepo: productIngredientRepo,
txManager: txManager,
}
}
@@ -49,47 +61,133 @@ func (s *OrderServiceImpl) CreateOrder(ctx context.Context, req *models.CreateOr
}
}
response, err := s.orderProcessor.CreateOrder(ctx, req, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to create order: %w", err)
}
var response *models.OrderResponse
var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
if req.TableID != nil {
if err := s.occupyTableWithOrder(ctx, *req.TableID, response.ID); err != nil {
fmt.Printf("Warning: failed to occupy table %s with order %s: %v\n", *req.TableID, response.ID, err)
// Use transaction to ensure atomicity
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Create the order
orderResp, err := s.orderProcessor.CreateOrder(txCtx, req, organizationID)
if err != nil {
return fmt.Errorf("failed to create order: %w", err)
}
response = orderResp
// Create ingredient transactions for each order item
ingredientTransactions, err = s.createIngredientTransactions(txCtx, response.ID, response.OrderItems)
if err != nil {
return fmt.Errorf("failed to create ingredient transactions: %w", err)
}
// Bulk create ingredient transactions
if len(ingredientTransactions) > 0 {
_, err = s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(txCtx, ingredientTransactions)
if err != nil {
return fmt.Errorf("failed to bulk create ingredient transactions: %w", err)
}
}
// Occupy table if specified
if req.TableID != nil {
if err := s.occupyTableWithOrder(txCtx, *req.TableID, response.ID); err != nil {
// Log warning but don't fail the transaction
fmt.Printf("Warning: failed to occupy table %s with order %s: %v\n", *req.TableID, response.ID, err)
}
}
return nil
})
if err != nil {
return nil, err
}
return response, nil
}
// createIngredientTransactions creates ingredient transactions for order items efficiently
func (s *OrderServiceImpl) createIngredientTransactions(ctx context.Context, orderID uuid.UUID, orderItems []models.OrderItemResponse) ([]*contract.CreateOrderIngredientTransactionRequest, error) {
appCtx := appcontext.FromGinContext(ctx)
organizationID := appCtx.OrganizationID
var allTransactions []*contract.CreateOrderIngredientTransactionRequest
for _, orderItem := range orderItems {
// Get product ingredients for this product
productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err)
}
if len(productIngredients) == 0 {
continue // Skip if no ingredients
}
// Calculate waste quantities
transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity))
if err != nil {
return nil, fmt.Errorf("failed to calculate waste quantities for product %s: %w", err)
}
// Set common fields for all transactions
for _, transaction := range transactions {
transaction.OrderID = orderID
transaction.OrderItemID = &orderItem.ID
transaction.ProductID = orderItem.ProductID
transaction.ProductVariantID = orderItem.ProductVariantID
}
allTransactions = append(allTransactions, transactions...)
}
return allTransactions, nil
}
func (s *OrderServiceImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) {
// Validate inputs
if orderID == uuid.Nil {
return nil, fmt.Errorf("invalid order ID")
}
// Validate request
if err := s.validateAddToOrderRequest(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err)
}
// Process adding items to order
response, err := s.orderProcessor.AddToOrder(ctx, orderID, req)
var response *models.AddToOrderResponse
var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
addResp, err := s.orderProcessor.AddToOrder(txCtx, orderID, req)
if err != nil {
return fmt.Errorf("failed to add items to order: %w", err)
}
response = addResp
ingredientTransactions, err = s.createIngredientTransactions(txCtx, orderID, response.AddedItems)
if err != nil {
return fmt.Errorf("failed to create ingredient transactions: %w", err)
}
if len(ingredientTransactions) > 0 {
_, err = s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(txCtx, ingredientTransactions)
if err != nil {
return fmt.Errorf("failed to bulk create ingredient transactions: %w", err)
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to add items to order: %w", err)
return nil, err
}
return response, nil
}
func (s *OrderServiceImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) {
// Validate request
if err := s.validateUpdateOrderRequest(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err)
}
// Process order update
response, err := s.orderProcessor.UpdateOrder(ctx, id, req)
if err != nil {
return nil, fmt.Errorf("failed to update order: %w", err)
@@ -137,9 +235,7 @@ func (s *OrderServiceImpl) VoidOrder(ctx context.Context, req *models.VoidOrderR
return fmt.Errorf("failed to void order: %w", err)
}
// Release table if order is voided
if err := s.handleTableReleaseOnVoid(ctx, req.OrderID); err != nil {
// Log the error but don't fail the void operation
fmt.Printf("Warning: failed to handle table release for voided order %s: %v\n", req.OrderID, err)
}
@@ -547,3 +643,79 @@ func (s *OrderServiceImpl) handleTableReleaseOnVoid(ctx context.Context, orderID
return nil
}
func (s *OrderServiceImpl) createOrderIngredientTransactions(ctx context.Context, order *models.Order, orderItems []*models.OrderItem) error {
for _, orderItem := range orderItems {
productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, order.OrganizationID)
if err != nil {
return fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err)
}
if len(productIngredients) == 0 {
continue // Skip if no ingredients
}
// Calculate waste quantities using the utility function
transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity))
if err != nil {
return fmt.Errorf("failed to calculate waste quantities for product %s: %w", orderItem.ProductID, err)
}
// Set common fields for all transactions
for _, transaction := range transactions {
transaction.OrderID = order.ID
transaction.OrderItemID = &orderItem.ID
transaction.ProductID = orderItem.ProductID
transaction.ProductVariantID = orderItem.ProductVariantID
}
// Bulk create transactions
if len(transactions) > 0 {
_, err := s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(ctx, transactions)
if err != nil {
return fmt.Errorf("failed to create order ingredient transactions for product %s: %w", orderItem.ProductID, err)
}
}
}
return nil
}
// calculateWasteQuantities calculates gross, net, and waste quantities for product ingredients
func (s *OrderServiceImpl) calculateWasteQuantities(productIngredients []*entities.ProductIngredient, quantity float64) ([]*contract.CreateOrderIngredientTransactionRequest, error) {
if len(productIngredients) == 0 {
return []*contract.CreateOrderIngredientTransactionRequest{}, nil
}
transactions := make([]*contract.CreateOrderIngredientTransactionRequest, 0, len(productIngredients))
for _, pi := range productIngredients {
// Calculate net quantity (actual quantity needed for the product)
netQty := pi.Quantity * quantity
// Calculate gross quantity (including waste)
wasteMultiplier := 1 + (pi.WastePercentage / 100)
grossQty := netQty * wasteMultiplier
// Calculate waste quantity
wasteQty := grossQty - netQty
// Get unit name from ingredient
unitName := "unit" // default
if pi.Ingredient != nil && pi.Ingredient.Unit != nil {
unitName = pi.Ingredient.Unit.Name
}
transaction := &contract.CreateOrderIngredientTransactionRequest{
IngredientID: pi.IngredientID,
GrossQty: util.RoundToDecimalPlaces(grossQty, 3),
NetQty: util.RoundToDecimalPlaces(netQty, 3),
WasteQty: util.RoundToDecimalPlaces(wasteQty, 3),
Unit: unitName,
}
transactions = append(transactions, transaction)
}
return transactions, nil
}