package order import ( "enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/entity" "fmt" "github.com/pkg/errors" "go.uber.org/zap" ) func (s *orderSvc) PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error { order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) if err != nil { logger.ContextLogger(ctx).Error("failed to find order for partial refund", zap.Error(err)) return err } if order.Status != "PAID" && order.Status != "PARTIAL" { return errors.New("only paid order can be partially refunded") } refundedAmount := 0.0 orderItemMap := make(map[int64]*entity.OrderItem) for _, item := range order.OrderItems { orderItemMap[item.ID] = &item } for _, refundItem := range items { orderItem, exists := orderItemMap[refundItem.OrderItemID] if !exists { return errors.New(fmt.Sprintf("order item %d not found", refundItem.OrderItemID)) } if refundItem.Quantity > orderItem.Quantity { return errors.New(fmt.Sprintf("refund quantity %d exceeds available quantity %d for item %d", refundItem.Quantity, orderItem.Quantity, refundItem.OrderItemID)) } refundedAmount += orderItem.Price * float64(refundItem.Quantity) } for _, refundItem := range items { orderItem := orderItemMap[refundItem.OrderItemID] newQuantity := orderItem.Quantity - refundItem.Quantity if newQuantity == 0 { err = s.repo.UpdateOrderItem(ctx, refundItem.OrderItemID, 0) } else { err = s.repo.UpdateOrderItem(ctx, refundItem.OrderItemID, newQuantity) } if err != nil { logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err)) return err } } remainingAmount := order.Amount - refundedAmount remainingTax := (remainingAmount / order.Amount) * order.Tax remainingTotal := remainingAmount + remainingTax err = s.repo.UpdateOrderTotals(ctx, orderID, remainingAmount, remainingTax, remainingTotal) if err != nil { logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err)) return err } newStatus := "PARTIAL" if remainingAmount <= 0 { newStatus = "REFUNDED" } 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 } refundTransaction, err := s.createRefundTransaction(ctx, order, reason) if err != nil { logger.ContextLogger(ctx).Error("failed to create refund transaction", zap.Error(err)) return err } refundTransaction.Amount = -refundedAmount _, err = s.transaction.Create(ctx, refundTransaction) if err != nil { logger.ContextLogger(ctx).Error("failed to update refund transaction", zap.Error(err)) return err } logger.ContextLogger(ctx).Info("partial refund processed successfully", zap.Int64("orderID", orderID), zap.String("reason", reason), zap.Float64("refundedAmount", refundedAmount), zap.String("refundTransactionID", refundTransaction.ID)) return nil } // VoidOrderRequest handles voiding orders (for ongoing orders) or specific items func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error { order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) if err != nil { logger.ContextLogger(ctx).Error("failed to find order for void", zap.Error(err)) return err } if order.Status != "NEW" && order.Status != "PENDING" { return errors.New("only new or pending orders can be voided") } if voidType == "ALL" { // 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 orderItemMap := make(map[int64]*entity.OrderItem) for i := range order.OrderItems { orderItemMap[order.OrderItems[i].ID] = &order.OrderItems[i] } for _, voidItem := range items { orderItem, exists := orderItemMap[voidItem.OrderItemID] if !exists { 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)) } } for _, voidItem := range items { orderItem := orderItemMap[voidItem.OrderItemID] // 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 } } updatedOrder, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) if err != nil { logger.ContextLogger(ctx).Error("failed to fetch updated order for recalculation", zap.Error(err)) return err } 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, }) } } 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 } } } logger.ContextLogger(ctx).Info("order voided successfully", zap.Int64("orderID", orderID), zap.String("reason", reason), zap.String("voidType", voidType)) return nil } 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)) return nil, err } if order.Status != "NEW" && order.Status != "PENDING" { return nil, errors.New("only new or pending orders can be split") } var splitOrder *entity.Order if splitType == "ITEM" { splitOrder, err = s.splitByItems(ctx, order, items) } else if splitType == "AMOUNT" { splitOrder, err = s.splitByAmount(ctx, order, amount) } if err != nil { logger.ContextLogger(ctx).Error("failed to split bill", zap.Error(err)) return nil, err } logger.ContextLogger(ctx).Info("bill split successfully", zap.Int64("orderID", orderID), zap.String("splitType", splitType), zap.Int64("splitOrderID", splitOrder.ID)) return splitOrder, nil } 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 i := range originalOrder.OrderItems { orderItemMap[originalOrder.OrderItems[i].ID] = &originalOrder.OrderItems[i] } assignedItems := make(map[int64]bool) for _, item := range items { orderItem, exists := orderItemMap[item.OrderItemID] if !exists { return nil, errors.New(fmt.Sprintf("order item %d not found", item.OrderItemID)) } if item.Quantity > orderItem.Quantity { return nil, errors.New(fmt.Sprintf("split quantity %d exceeds available quantity %d for item %d", item.Quantity, orderItem.Quantity, item.OrderItemID)) } if assignedItems[item.OrderItemID] { return nil, errors.New(fmt.Sprintf("order item %d is already assigned to another split", item.OrderItemID)) } assignedItems[item.OrderItemID] = true splitOrderItems = append(splitOrderItems, entity.OrderItem{ ItemID: orderItem.ItemID, ItemType: orderItem.ItemType, Price: orderItem.Price, ItemName: orderItem.ItemName, Quantity: item.Quantity, CreatedBy: originalOrder.CreatedBy, Product: orderItem.Product, Notes: orderItem.Notes, }) } splitAmount := 0.0 for _, item := range splitOrderItems { splitAmount += item.Price * float64(item.Quantity) } splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax splitTotal := splitAmount + splitTax splitOrder := &entity.Order{ PartnerID: originalOrder.PartnerID, CustomerID: originalOrder.CustomerID, CustomerName: originalOrder.CustomerName, Status: "PAID", Amount: splitAmount, Tax: splitTax, Total: splitTotal, Source: originalOrder.Source, CreatedBy: originalOrder.CreatedBy, OrderItems: splitOrderItems, OrderType: originalOrder.OrderType, TableNumber: originalOrder.TableNumber, CashierSessionID: originalOrder.CashierSessionID, } createdOrder, err := s.repo.Create(ctx, splitOrder) if err != nil { logger.ContextLogger(ctx).Error("failed to create split order", zap.Error(err)) return nil, err } for _, item := range items { orderItem := orderItemMap[item.OrderItemID] newQuantity := orderItem.Quantity - item.Quantity if newQuantity == 0 { err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0) } else { err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, newQuantity) } if err != nil { logger.ContextLogger(ctx).Error("failed to update original order item", zap.Error(err)) return nil, err } } remainingAmount := originalOrder.Amount - splitAmount remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax remainingTotal := remainingAmount + remainingTax 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)) return nil, err } return createdOrder, nil } // splitByAmount splits the order by assigning specific amounts to each split 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", amount, originalOrder.Total)) } // For amount-based split, we create a new order with all items var splitOrderItems []entity.OrderItem for _, item := range originalOrder.OrderItems { splitOrderItems = append(splitOrderItems, entity.OrderItem{ ItemID: item.ItemID, ItemType: item.ItemType, Price: item.Price, ItemName: item.ItemName, Quantity: item.Quantity, CreatedBy: originalOrder.CreatedBy, Product: item.Product, Notes: item.Notes, }) } splitAmount := amount splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax splitTotal := splitAmount + splitTax splitOrder := &entity.Order{ PartnerID: originalOrder.PartnerID, CustomerID: originalOrder.CustomerID, CustomerName: originalOrder.CustomerName, Status: "PAID", Amount: splitAmount, Tax: splitTax, Total: splitTotal, Source: originalOrder.Source, CreatedBy: originalOrder.CreatedBy, OrderItems: splitOrderItems, OrderType: originalOrder.OrderType, TableNumber: originalOrder.TableNumber, CashierSessionID: originalOrder.CashierSessionID, } createdOrder, err := s.repo.Create(ctx, splitOrder) if err != nil { logger.ContextLogger(ctx).Error("failed to create split order", zap.Error(err)) return nil, err } // Adjust original order amount 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)) return nil, err } return createdOrder, nil }