488 lines
16 KiB
Go
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
|
|
}
|