Add void print

This commit is contained in:
aditya.siregar
2025-06-24 02:47:44 +07:00
parent 53014d90ab
commit 1201b2e45b
18 changed files with 2033 additions and 12 deletions
@@ -0,0 +1,403 @@
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
}
// Only allow voiding for NEW, PENDING orders
if order.Status != "NEW" && order.Status != "PENDING" {
return errors.New("only new or pending orders can be voided")
}
if voidType == "ALL" {
// Void entire order
err = s.repo.UpdateOrder(ctx, orderID, "VOIDED", reason)
if err != nil {
logger.ContextLogger(ctx).Error("failed to void order", zap.Error(err))
return err
}
} else if voidType == "ITEM" {
// Void specific items
voidedAmount := 0.0
orderItemMap := make(map[int64]*entity.OrderItem)
for _, item := range order.OrderItems {
orderItemMap[item.ID] = &item
}
for _, voidItem := range items {
orderItem, exists := orderItemMap[voidItem.OrderItemID]
if !exists {
return errors.New(fmt.Sprintf("order item %d not found", 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))
}
voidedAmount += orderItem.Price * float64(voidItem.Quantity)
}
// Update order items with reduced quantities
for _, voidItem := range items {
orderItem := orderItemMap[voidItem.OrderItemID]
newQuantity := orderItem.Quantity - voidItem.Quantity
if newQuantity == 0 {
// Remove item completely
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, 0)
} else {
// Update 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
}
}
// Recalculate order totals
remainingAmount := order.Amount - voidedAmount
remainingTax := (remainingAmount / order.Amount) * order.Tax
remainingTotal := remainingAmount + remainingTax
// Update order totals
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
}
// Update order status to PARTIAL if some items remain, otherwise to VOIDED
newStatus := "PARTIAL"
if remainingAmount <= 0 {
newStatus = "VOIDED"
}
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
}
}
logger.ContextLogger(ctx).Info("order voided successfully",
zap.Int64("orderID", orderID),
zap.String("reason", reason),
zap.String("voidType", voidType))
return nil
}
// SplitBillRequest handles splitting bills by items or amounts
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider 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, paymentMethod, paymentProvider, items)
} else if splitType == "AMOUNT" {
splitOrder, err = s.splitByAmount(ctx, order, paymentMethod, paymentProvider, 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, paymentMethod string, paymentProvider string, items []entity.SplitBillItem) (*entity.Order, error) {
var splitOrderItems []entity.OrderItem
orderItemMap := make(map[int64]*entity.OrderItem)
for _, item := range originalOrder.OrderItems {
orderItemMap[item.ID] = &item
}
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
// Create new PAID order for the split
splitOrder := &entity.Order{
PartnerID: originalOrder.PartnerID,
CustomerID: originalOrder.CustomerID,
CustomerName: originalOrder.CustomerName,
Status: "PAID",
Amount: splitAmount,
Tax: splitTax,
Total: splitTotal,
PaymentType: paymentMethod,
PaymentProvider: paymentProvider,
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 items (reduce quantities)
for _, item := range items {
orderItem := orderItemMap[item.OrderItemID]
newQuantity := orderItem.Quantity - item.Quantity
if newQuantity == 0 {
// Remove item completely
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0)
} else {
// Update quantity
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
}
}
// Recalculate original order totals
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
}
// splitByAmount splits the order by assigning specific amounts to each split
func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, 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
// Create new PAID order for the split
splitOrder := &entity.Order{
PartnerID: originalOrder.PartnerID,
CustomerID: originalOrder.CustomerID,
CustomerName: originalOrder.CustomerName,
Status: "PAID",
Amount: splitAmount,
Tax: splitTax,
Total: splitTotal,
PaymentType: paymentMethod,
PaymentProvider: paymentProvider,
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
}
@@ -38,7 +38,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
customerID := int64(0)
if req.CustomerID != nil {
if req.CustomerID != nil && *req.CustomerID != 0 {
customer, err := s.customer.GetCustomer(ctx, *req.CustomerID)
if err != nil {
logger.ContextLogger(ctx).Error("customer is not found", zap.Error(err))
+60 -3
View File
@@ -38,9 +38,9 @@ func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context,
}
func (s *orderSvc) RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error {
order, err := s.repo.FindByIDAndPartnerID(ctx, partnerID, orderID)
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create order", zap.Error(err))
logger.ContextLogger(ctx).Error("failed to find order for refund", zap.Error(err))
return err
}
@@ -48,7 +48,31 @@ func (s *orderSvc) RefundRequest(ctx mycontext.Context, partnerID, orderID int64
return errors.New("only paid order can be refund")
}
return s.repo.UpdateOrder(ctx, order.ID, "REFUNDED", reason)
err = s.repo.UpdateOrder(ctx, order.ID, "REFUNDED", 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
}
if order.CustomerID != nil && *order.CustomerID > 0 {
err = s.reverseCustomerVouchers(ctx, *order.CustomerID, int64(order.Total), order.ID)
if err != nil {
logger.ContextLogger(ctx).Warn("failed to reverse customer vouchers", zap.Error(err))
}
}
logger.ContextLogger(ctx).Info("refund processed successfully",
zap.Int64("orderID", orderID),
zap.String("reason", reason),
zap.String("refundTransactionID", refundTransaction.ID))
return nil
}
func (s *orderSvc) processPostOrderActions(
@@ -262,3 +286,36 @@ func formatPaymentMethod(method string) string {
}
return method
}
func (s *orderSvc) createRefundTransaction(ctx mycontext.Context, order *entity.Order, reason string) (*entity.Transaction, error) {
transaction := &entity.Transaction{
OrderID: order.ID,
Amount: -order.Total,
PaymentMethod: order.PaymentType,
Status: "REFUND",
CreatedAt: constants.TimeNow(),
PartnerID: order.PartnerID,
TransactionType: "REFUND",
CreatedBy: ctx.RequestedBy(),
UpdatedBy: ctx.RequestedBy(),
}
_, err := s.transaction.Create(ctx, transaction)
return transaction, err
}
func (s *orderSvc) reverseCustomerVouchers(ctx mycontext.Context, customerID int64, total int64, orderID int64) error {
// Find vouchers associated with this order and reverse them
// This is a simplified implementation - in production you might want to track voucher-order relationships
logger.ContextLogger(ctx).Info("reversing customer vouchers",
zap.Int64("customerID", customerID),
zap.Int64("orderID", orderID))
// TODO: Implement voucher reversal logic
// This would involve:
// 1. Finding vouchers created for this order
// 2. Marking them as reversed/cancelled
// 3. Optionally adjusting customer points
return nil
}
+6
View File
@@ -13,6 +13,8 @@ type Repository interface {
FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error)
UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
GetOrderPaymentMethodBreakdown(
ctx mycontext.Context,
@@ -67,6 +69,9 @@ type Service interface {
ExecuteOrderInquiry(ctx mycontext.Context,
token string, paymentMethod, paymentProvider string, inProgressOrderID int64) (*entity.OrderResponse, error)
RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error
PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error
VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error)
GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
CalculateOrderTotals(
ctx mycontext.Context,
@@ -104,6 +109,7 @@ type Service interface {
) ([]entity.PopularProductItem, error)
GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error)
GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error)
}
type Config interface {
@@ -27,3 +27,15 @@ func (s *orderSvc) GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerI
return orders, nil
}
func (s *orderSvc) GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error) {
order, err := s.repo.FindByID(ctx, orderID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get order by ID",
zap.Error(err),
zap.Int64("orderID", orderID))
return nil, errors.Wrap(err, "failed to get order")
}
return order, nil
}