apskel-pos-backend/internal/services/v2/order/advanced_order_management.go
2025-06-27 13:01:39 +07:00

488 lines
16 KiB
Go

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
}