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" ) type OrderService interface { CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) SplitBill(ctx context.Context, req *models.SplitBillRequest) (*models.SplitBillResponse, error) } type OrderServiceImpl struct { orderProcessor processor.OrderProcessor tableRepo repository.TableRepositoryInterface orderIngredientTransactionService *OrderIngredientTransactionService orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor productRecipeRepo repository.ProductRecipeRepository txManager *repository.TxManager } func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productRecipeRepo repository.ProductRecipeRepository, txManager *repository.TxManager) *OrderServiceImpl { return &OrderServiceImpl{ orderProcessor: orderProcessor, tableRepo: tableRepo, orderIngredientTransactionService: orderIngredientTransactionService, orderIngredientTransactionProcessor: orderIngredientTransactionProcessor, productRecipeRepo: productRecipeRepo, txManager: txManager, } } func (s *OrderServiceImpl) CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) { if err := s.validateCreateOrderRequest(req, organizationID); err != nil { return nil, fmt.Errorf("validation error: %w", err) } if req.TableID != nil { if err := s.validateTable(ctx, req); err != nil { return nil, fmt.Errorf("table validation failed: %w", err) } } var response *models.OrderResponse var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest // 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 recipes for this product productRecipes, err := s.productRecipeRepo.GetByProductID(ctx, orderItem.ProductID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipes for product %s: %w", orderItem.ProductID, err) } if len(productRecipes) == 0 { continue // Skip if no recipes } // Calculate waste quantities transactions, err := s.calculateWasteQuantities(productRecipes, 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) { if orderID == uuid.Nil { return nil, fmt.Errorf("invalid order ID") } if err := s.validateAddToOrderRequest(req); err != nil { return nil, fmt.Errorf("validation error: %w", err) } 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, err } return response, nil } func (s *OrderServiceImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) { if err := s.validateUpdateOrderRequest(req); err != nil { return nil, fmt.Errorf("validation error: %w", err) } response, err := s.orderProcessor.UpdateOrder(ctx, id, req) if err != nil { return nil, fmt.Errorf("failed to update order: %w", err) } return response, nil } func (s *OrderServiceImpl) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) { if id == uuid.Nil { return nil, fmt.Errorf("invalid order ID") } response, err := s.orderProcessor.GetOrderByID(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get order: %w", err) } return response, nil } func (s *OrderServiceImpl) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) { if err := s.validateListOrdersRequest(req); err != nil { return nil, fmt.Errorf("validation error: %w", err) } response, err := s.orderProcessor.ListOrders(ctx, req) if err != nil { return nil, fmt.Errorf("failed to list orders: %w", err) } return response, nil } func (s *OrderServiceImpl) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error { if req.OrderID == uuid.Nil { return fmt.Errorf("invalid order ID") } if voidedBy == uuid.Nil { return fmt.Errorf("invalid user ID") } if err := s.orderProcessor.VoidOrder(ctx, req, voidedBy); err != nil { return fmt.Errorf("failed to void order: %w", err) } if err := s.handleTableReleaseOnVoid(ctx, req.OrderID); err != nil { fmt.Printf("Warning: failed to handle table release for voided order %s: %v\n", req.OrderID, err) } return nil } func (s *OrderServiceImpl) RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error { // Validate inputs if id == uuid.Nil { return fmt.Errorf("invalid order ID") } if refundedBy == uuid.Nil { return fmt.Errorf("invalid user ID") } // Validate refund request if err := s.validateRefundOrderRequest(req); err != nil { return fmt.Errorf("validation error: %w", err) } // Process order refund if err := s.orderProcessor.RefundOrder(ctx, id, req, refundedBy); err != nil { return fmt.Errorf("failed to refund order: %w", err) } return nil } func (s *OrderServiceImpl) CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) { if err := s.validateCreatePaymentRequest(req); err != nil { return nil, fmt.Errorf("validation error: %w", err) } response, err := s.orderProcessor.CreatePayment(ctx, req) if err != nil { return nil, fmt.Errorf("failed to create payment: %w", err) } if err := s.handleTableReleaseOnPayment(ctx, req.OrderID); err != nil { fmt.Printf("Warning: failed to handle table release for order %s: %v\n", req.OrderID, err) } return response, nil } func (s *OrderServiceImpl) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error { // Validate inputs if paymentID == uuid.Nil { return fmt.Errorf("invalid payment ID") } if refundAmount <= 0 { return fmt.Errorf("refund amount must be greater than zero") } if refundedBy == uuid.Nil { return fmt.Errorf("invalid user ID") } // Process payment refund if err := s.orderProcessor.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil { return fmt.Errorf("failed to refund payment: %w", err) } return nil } func (s *OrderServiceImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) { // Validate inputs if orderID == uuid.Nil { return nil, fmt.Errorf("invalid order ID") } if organizationID == uuid.Nil { return nil, fmt.Errorf("invalid organization ID") } // Validate request if err := s.validateSetOrderCustomerRequest(req); err != nil { return nil, fmt.Errorf("validation error: %w", err) } // Process setting customer for order response, err := s.orderProcessor.SetOrderCustomer(ctx, orderID, req, organizationID) if err != nil { return nil, fmt.Errorf("failed to set customer for order: %w", err) } return response, nil } func (s *OrderServiceImpl) validateAddToOrderRequest(req *models.AddToOrderRequest) error { if req == nil { return fmt.Errorf("request cannot be nil") } if len(req.OrderItems) == 0 { return fmt.Errorf("must add at least one item") } for i, item := range req.OrderItems { if item.ProductID == uuid.Nil { return fmt.Errorf("product ID is required for item %d", i+1) } if item.Quantity <= 0 { return fmt.Errorf("quantity must be greater than zero for item %d", i+1) } if item.UnitPrice != nil && *item.UnitPrice < 0 { return fmt.Errorf("unit price cannot be negative for item %d", i+1) } } return nil } func (s *OrderServiceImpl) validateCreateOrderRequest(req *models.CreateOrderRequest, organizationID uuid.UUID) error { if req == nil { return fmt.Errorf("request cannot be nil") } if organizationID == uuid.Nil { return fmt.Errorf("organization ID is required") } if req.OutletID == uuid.Nil { return fmt.Errorf("outlet ID is required") } if req.UserID == uuid.Nil { return fmt.Errorf("user ID is required") } // Validate table ID if provided if req.TableID != nil && *req.TableID == uuid.Nil { return fmt.Errorf("table ID cannot be nil if provided") } if len(req.OrderItems) == 0 { return fmt.Errorf("order must have at least one item") } for i, item := range req.OrderItems { if item.ProductID == uuid.Nil { return fmt.Errorf("product ID is required for item %d", i+1) } if item.Quantity <= 0 { return fmt.Errorf("quantity must be greater than zero for item %d", i+1) } if item.UnitPrice != nil && *item.UnitPrice < 0 { return fmt.Errorf("unit price cannot be negative for item %d", i+1) } } return nil } func (s *OrderServiceImpl) validateUpdateOrderRequest(req *models.UpdateOrderRequest) error { if req == nil { return fmt.Errorf("request cannot be nil") } if req.DiscountAmount != nil && *req.DiscountAmount < 0 { return fmt.Errorf("discount amount cannot be negative") } return nil } func (s *OrderServiceImpl) validateListOrdersRequest(req *models.ListOrdersRequest) error { if req == nil { return fmt.Errorf("request cannot be nil") } if req.Page < 1 { return fmt.Errorf("page must be greater than zero") } if req.Limit < 1 || req.Limit > 100 { return fmt.Errorf("limit must be between 1 and 100") } return nil } func (s *OrderServiceImpl) validateRefundOrderRequest(req *models.RefundOrderRequest) error { if req == nil { return fmt.Errorf("request cannot be nil") } // Must have either refund amount or order items if req.RefundAmount == nil && len(req.OrderItems) == 0 { return fmt.Errorf("must specify either refund amount or order items to refund") } // Cannot have both refund amount and order items if req.RefundAmount != nil && len(req.OrderItems) > 0 { return fmt.Errorf("cannot specify both refund amount and order items") } if req.RefundAmount != nil && *req.RefundAmount <= 0 { return fmt.Errorf("refund amount must be greater than zero") } // Validate order items if provided for i, item := range req.OrderItems { if item.OrderItemID == uuid.Nil { return fmt.Errorf("order item ID is required for item %d", i+1) } if item.RefundQuantity < 0 { return fmt.Errorf("refund quantity cannot be negative for item %d", i+1) } if item.RefundAmount != nil && *item.RefundAmount < 0 { return fmt.Errorf("refund amount cannot be negative for item %d", i+1) } } return nil } func (s *OrderServiceImpl) validateCreatePaymentRequest(req *models.CreatePaymentRequest) error { if req == nil { return fmt.Errorf("request cannot be nil") } if req.OrderID == uuid.Nil { return fmt.Errorf("order ID is required") } if req.PaymentMethodID == uuid.Nil { return fmt.Errorf("payment method ID is required") } if req.Amount <= 0 { return fmt.Errorf("payment amount must be greater than zero") } if len(req.PaymentOrderItems) > 0 { totalItemAmount := float64(0) for i, item := range req.PaymentOrderItems { if item.OrderItemID == uuid.Nil { return fmt.Errorf("order item ID is required for payment item %d", i+1) } if item.Amount <= 0 { return fmt.Errorf("payment item amount must be greater than zero for item %d", i+1) } totalItemAmount += item.Amount } if totalItemAmount != req.Amount { return fmt.Errorf("sum of payment item amounts must equal total payment amount") } } return nil } func (s *OrderServiceImpl) validateSetOrderCustomerRequest(req *models.SetOrderCustomerRequest) error { if req == nil { return fmt.Errorf("request cannot be nil") } if req.CustomerID == uuid.Nil { return fmt.Errorf("customer ID is required") } return nil } func (s *OrderServiceImpl) SplitBill(ctx context.Context, req *models.SplitBillRequest) (*models.SplitBillResponse, error) { if err := s.validateSplitBillRequest(req); err != nil { return nil, fmt.Errorf("validation error: %w", err) } response, err := s.orderProcessor.SplitBill(ctx, req) if err != nil { return nil, fmt.Errorf("failed to split bill: %w", err) } if err := s.handleTableReleaseOnPayment(ctx, req.OrderID); err != nil { fmt.Printf("Warning: failed to handle table release for order %s after split bill: %v\n", req.OrderID, err) } return response, nil } func (s *OrderServiceImpl) validateSplitBillRequest(req *models.SplitBillRequest) error { if req == nil { return fmt.Errorf("request cannot be nil") } if req.OrderID == uuid.Nil { return fmt.Errorf("order ID is required") } if req.PaymentMethodID == uuid.Nil { return fmt.Errorf("payment ID is required") } if req.Type != "ITEM" && req.Type != "AMOUNT" { return fmt.Errorf("split type must be either ITEM or AMOUNT") } if req.Type == "ITEM" { if len(req.Items) == 0 { return fmt.Errorf("items are required when splitting by ITEM") } totalItemAmount := 0 for i, item := range req.Items { if item.OrderItemID == uuid.Nil { return fmt.Errorf("order item ID is required for item %d", i+1) } if item.Quantity <= 0 { return fmt.Errorf("quantity must be greater than zero for item %d", i+1) } totalItemAmount += item.Quantity } if totalItemAmount <= 0 { return fmt.Errorf("total item amount must be greater than zero") } } if req.Type == "AMOUNT" { if req.Amount <= 0 { return fmt.Errorf("amount must be greater than zero when splitting by AMOUNT") } } return nil } // validateTable validates that the table exists and is available for occupation func (s *OrderServiceImpl) validateTable(ctx context.Context, req *models.CreateOrderRequest) error { // Validate table exists and is available table, err := s.tableRepo.GetByID(ctx, *req.TableID) if err != nil { return fmt.Errorf("table not found: %w", err) } // Check if table belongs to the same outlet if table.OutletID != req.OutletID { return fmt.Errorf("table does not belong to the specified outlet") } // Check if table is available for occupation if !table.CanBeOccupied() { return fmt.Errorf("table is not available for occupation (current status: %s)", table.Status) } return nil } func (s *OrderServiceImpl) occupyTableWithOrder(ctx context.Context, tableID, orderID uuid.UUID) error { startTime := time.Now() if err := s.tableRepo.OccupyTable(ctx, tableID, orderID, &startTime); err != nil { return fmt.Errorf("failed to occupy table: %w", err) } return nil } func (s *OrderServiceImpl) handleTableReleaseOnPayment(ctx context.Context, orderID uuid.UUID) error { order, err := s.orderProcessor.GetOrderByID(ctx, orderID) if err != nil { return fmt.Errorf("failed to get order: %w", err) } if order.PaymentStatus == "completed" { table, err := s.tableRepo.GetByOrderID(ctx, orderID) if err != nil { return nil } if table != nil { if err := s.tableRepo.ReleaseTable(ctx, table.ID, order.TotalAmount); err != nil { return fmt.Errorf("failed to release table: %w", err) } } } return nil } // handleTableReleaseOnVoid releases the table when an order is voided func (s *OrderServiceImpl) handleTableReleaseOnVoid(ctx context.Context, orderID uuid.UUID) error { table, err := s.tableRepo.GetByOrderID(ctx, orderID) if err != nil { // Table might not exist or not be occupied, which is fine return nil } if table != nil { if err := s.tableRepo.ReleaseTable(ctx, table.ID, 0); err != nil { return fmt.Errorf("failed to release table: %w", err) } } return nil } func (s *OrderServiceImpl) createOrderIngredientTransactions(ctx context.Context, order *models.Order, orderItems []*models.OrderItem) error { for _, orderItem := range orderItems { productRecipes, err := s.productRecipeRepo.GetByProductID(ctx, orderItem.ProductID, order.OrganizationID) if err != nil { return fmt.Errorf("failed to get product recipes for product %s: %w", orderItem.ProductID, err) } if len(productRecipes) == 0 { continue // Skip if no recipes } // Calculate waste quantities using the utility function transactions, err := s.calculateWasteQuantities(productRecipes, 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 recipes func (s *OrderServiceImpl) calculateWasteQuantities(productRecipes []*entities.ProductRecipe, quantity float64) ([]*contract.CreateOrderIngredientTransactionRequest, error) { if len(productRecipes) == 0 { return []*contract.CreateOrderIngredientTransactionRequest{}, nil } transactions := make([]*contract.CreateOrderIngredientTransactionRequest, 0, len(productRecipes)) for _, pr := range productRecipes { // Calculate net quantity (actual quantity needed for the product) netQty := pr.Quantity * quantity // Calculate gross quantity (including waste) wasteMultiplier := 1 + (pr.WastePercentage / 100) grossQty := netQty * wasteMultiplier // Calculate waste quantity wasteQty := grossQty - netQty // Get unit name from ingredient unitName := "unit" // default if pr.Ingredient != nil && pr.Ingredient.Unit != nil { unitName = pr.Ingredient.Unit.Name } transaction := &contract.CreateOrderIngredientTransactionRequest{ IngredientID: pr.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 }