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
+7 -4
View File
@@ -3,10 +3,13 @@ package order
type OrderStatus string
const (
New OrderStatus = "NEW"
Paid OrderStatus = "PAID"
Cancel OrderStatus = "CANCEL"
Pending OrderStatus = "PENDING"
New OrderStatus = "NEW"
Paid OrderStatus = "PAID"
Cancel OrderStatus = "CANCEL"
Pending OrderStatus = "PENDING"
Refunded OrderStatus = "REFUNDED"
Voided OrderStatus = "VOIDED"
Partial OrderStatus = "PARTIAL"
)
func (b OrderStatus) toString() string {
@@ -6,6 +6,7 @@ const (
New PaymentStatus = "NEW"
Paid PaymentStatus = "PAID"
Cancel PaymentStatus = "CANCEL"
Refund PaymentStatus = "REFUND"
)
func (b PaymentStatus) toString() string {
+24
View File
@@ -124,6 +124,30 @@ type OrderItemRequest struct {
Notes string `json:"notes"`
}
type PartialRefundItem struct {
OrderItemID int64 `json:"order_item_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
type VoidItem struct {
OrderItemID int64 `json:"order_item_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
type SplitBillSplit struct {
CustomerName string `json:"customer_name" validate:"required"`
CustomerID *int64 `json:"customer_id"`
Items []SplitBillItem `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
}
type SplitBillItem struct {
OrderItemID int64 `json:"order_item_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
CustomerName string `json:"customer_name" validate:"required"`
CustomerID *int64 `json:"customer_id"`
}
type OrderExecuteRequest struct {
CreatedBy int64
PartnerID int64
+431 -1
View File
@@ -30,12 +30,15 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route.POST("/inquiry", jwt, h.Inquiry)
route.POST("/execute", jwt, h.Execute)
route.POST("/refund", jwt, h.Refund)
route.POST("/partial-refund", jwt, h.PartialRefund)
route.POST("/void", jwt, h.VoidOrder)
route.POST("/split-bill", jwt, h.SplitBill)
route.GET("/history", jwt, h.GetOrderHistory)
route.GET("/refund-history", jwt, h.GetRefundHistory)
route.GET("/payment-analysis", jwt, h.GetPaymentMethodAnalysis)
route.GET("/revenue-overview", jwt, h.GetRevenueOverview)
route.GET("/sales-by-category", jwt, h.GetSalesByCategory)
route.GET("/popular-products", jwt, h.GetPopularProducts)
}
type InquiryRequest struct {
@@ -77,6 +80,123 @@ type RefundRequest struct {
Reason string `json:"reason" validate:"required"`
}
type PartialRefundRequest struct {
OrderID int64 `json:"order_id" validate:"required"`
Reason string `json:"reason" validate:"required"`
Items []PartialRefundItemRequest `json:"items" validate:"required,min=1,dive"`
}
type PartialRefundItemRequest struct {
OrderItemID int64 `json:"order_item_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
type VoidOrderRequest struct {
OrderID int64 `json:"order_id" validate:"required"`
Reason string `json:"reason" validate:"required"`
Type string `json:"type" validate:"required,oneof=ALL ITEM"`
Items []VoidItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
}
type VoidItemRequest struct {
OrderItemID int64 `json:"order_item_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
type SplitBillRequest struct {
OrderID int64 `json:"order_id" validate:"required"`
Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"`
PaymentMethod string `json:"payment_method" validate:"required"`
PaymentProvider string `json:"payment_provider"`
Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
}
type SplitBillItemRequest struct {
OrderItemID int64 `json:"order_item_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
type RefundResponse struct {
OrderID int64 `json:"order_id"`
Status string `json:"status"`
RefundAmount float64 `json:"refund_amount"`
Reason string `json:"reason"`
RefundedAt string `json:"refunded_at"`
CustomerName string `json:"customer_name"`
PaymentType string `json:"payment_type"`
}
type RefundHistoryResponse struct {
OrderID int64 `json:"order_id"`
CustomerName string `json:"customer_name"`
CustomerID *int64 `json:"customer_id"`
IsMember bool `json:"is_member"`
Status string `json:"status"`
Amount float64 `json:"amount"`
Total float64 `json:"total"`
PaymentType string `json:"payment_type"`
TableNumber string `json:"table_number"`
OrderType string `json:"order_type"`
CreatedAt string `json:"created_at"`
RefundedAt string `json:"refunded_at"`
Tax float64 `json:"tax"`
}
type PartialRefundResponse struct {
OrderID int64 `json:"order_id"`
Status string `json:"status"`
RefundedAmount float64 `json:"refunded_amount"`
RemainingAmount float64 `json:"remaining_amount"`
Reason string `json:"reason"`
RefundedAt string `json:"refunded_at"`
CustomerName string `json:"customer_name"`
PaymentType string `json:"payment_type"`
RefundedItems []RefundedItemResponse `json:"refunded_items"`
}
type RefundedItemResponse struct {
OrderItemID int64 `json:"order_item_id"`
ItemName string `json:"item_name"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
}
type VoidOrderResponse struct {
OrderID int64 `json:"order_id"`
Status string `json:"status"`
Reason string `json:"reason"`
VoidedAt string `json:"voided_at"`
CustomerName string `json:"customer_name"`
VoidedItems []VoidedItemResponse `json:"voided_items,omitempty"`
}
type VoidedItemResponse struct {
OrderItemID int64 `json:"order_item_id"`
ItemName string `json:"item_name"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
}
type SplitBillResponse struct {
OriginalOrderID int64 `json:"original_order_id"`
SplitOrders []SplitOrderResponse `json:"split_orders"`
SplitAt string `json:"split_at"`
}
type SplitOrderResponse struct {
OrderID int64 `json:"order_id"`
CustomerName string `json:"customer_name"`
CustomerID *int64 `json:"customer_id"`
Amount float64 `json:"amount"`
Total float64 `json:"total"`
Tax float64 `json:"tax"`
Status string `json:"status"`
Items []response.OrderItemResponse `json:"items"`
}
func (h *Handler) Inquiry(c *gin.Context) {
ctx := request.GetMyContext(c)
userID := ctx.RequestedBy()
@@ -181,9 +301,30 @@ func (h *Handler) Refund(c *gin.Context) {
return
}
order, err := h.service.GetOrderByID(ctx, req.OrderID)
if err != nil {
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Message: "Refund processed successfully",
})
return
}
refundResponse := RefundResponse{
OrderID: order.ID,
Status: order.Status,
RefundAmount: order.Total,
Reason: req.Reason,
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
CustomerName: order.CustomerName,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: refundResponse,
})
}
@@ -488,3 +629,292 @@ func (h *Handler) GetPopularProducts(c *gin.Context) {
Data: popularProducts,
})
}
func (h *Handler) GetRefundHistory(c *gin.Context) {
ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
limitStr := c.Query("limit")
offsetStr := c.Query("offset")
startDateStr := c.Query("start_date")
endDateStr := c.Query("end_date")
searchReq := entity.SearchRequest{}
limit := 20
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
if limit > 100 {
limit = 100
}
searchReq.Limit = limit
offset := 0
if offsetStr != "" {
parsedOffset, err := strconv.Atoi(offsetStr)
if err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
searchReq.Offset = offset
// Set status to REFUNDED to get only refunded orders
searchReq.Status = "REFUNDED"
if startDateStr != "" {
startDate, err := time.Parse(time.RFC3339, startDateStr)
if err == nil {
searchReq.Start = startDate
}
}
if endDateStr != "" {
endDate, err := time.Parse(time.RFC3339, endDateStr)
if err == nil {
searchReq.End = endDate
}
}
orders, total, err := h.service.GetOrderHistory(ctx, *partnerID, searchReq)
if err != nil {
response.ErrorWrapper(c, err)
return
}
responseData := []RefundHistoryResponse{}
for _, order := range orders {
responseData = append(responseData, RefundHistoryResponse{
OrderID: order.ID,
CustomerName: order.CustomerName,
CustomerID: order.CustomerID,
IsMember: order.IsMemberOrder(),
Status: order.Status,
Amount: order.Amount,
Total: order.Total,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
TableNumber: order.TableNumber,
OrderType: order.OrderType,
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
Tax: order.Tax,
})
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: responseData,
PagingMeta: &response.PagingMeta{
Page: offset + 1,
Total: int64(total),
Limit: limit,
},
})
}
func (h *Handler) PartialRefund(c *gin.Context) {
ctx := request.GetMyContext(c)
var req PartialRefundRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
validate := validator.New()
if err := validate.Struct(req); err != nil {
response.ErrorWrapper(c, err)
return
}
items := make([]entity.PartialRefundItem, len(req.Items))
for i, item := range req.Items {
items[i] = entity.PartialRefundItem{
OrderItemID: item.OrderItemID,
Quantity: item.Quantity,
}
}
err := h.service.PartialRefundRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason, items)
if err != nil {
response.ErrorWrapper(c, err)
return
}
// Get updated order to return details
order, err := h.service.GetOrderByID(ctx, req.OrderID)
if err != nil {
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Message: "Partial refund processed successfully",
})
return
}
// Calculate refunded amount
refundedAmount := 0.0
var refundedItems []RefundedItemResponse
for _, reqItem := range req.Items {
for _, orderItem := range order.OrderItems {
if orderItem.ID == reqItem.OrderItemID {
itemTotal := orderItem.Price * float64(reqItem.Quantity)
refundedAmount += itemTotal
refundedItems = append(refundedItems, RefundedItemResponse{
OrderItemID: orderItem.ID,
ItemName: orderItem.ItemName,
Quantity: reqItem.Quantity,
UnitPrice: orderItem.Price,
TotalPrice: itemTotal,
})
break
}
}
}
partialRefundResponse := PartialRefundResponse{
OrderID: order.ID,
Status: order.Status,
RefundedAmount: refundedAmount,
RemainingAmount: order.Total,
Reason: req.Reason,
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
CustomerName: order.CustomerName,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
RefundedItems: refundedItems,
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: partialRefundResponse,
})
}
func (h *Handler) VoidOrder(c *gin.Context) {
ctx := request.GetMyContext(c)
var req VoidOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
validate := validator.New()
if err := validate.Struct(req); err != nil {
response.ErrorWrapper(c, err)
return
}
// Convert request items to entity items
var items []entity.VoidItem
if req.Type == "ITEM" {
items = make([]entity.VoidItem, len(req.Items))
for i, item := range req.Items {
items[i] = entity.VoidItem{
OrderItemID: item.OrderItemID,
Quantity: item.Quantity,
}
}
}
err := h.service.VoidOrderRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason, req.Type, items)
if err != nil {
response.ErrorWrapper(c, err)
return
}
// Get updated order to return details
order, err := h.service.GetOrderByID(ctx, req.OrderID)
if err != nil {
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Message: "Order voided successfully",
})
return
}
var voidedItems []VoidedItemResponse
if req.Type == "ITEM" {
for _, reqItem := range req.Items {
for _, orderItem := range order.OrderItems {
if orderItem.ID == reqItem.OrderItemID {
itemTotal := orderItem.Price * float64(reqItem.Quantity)
voidedItems = append(voidedItems, VoidedItemResponse{
OrderItemID: orderItem.ID,
ItemName: orderItem.ItemName,
Quantity: reqItem.Quantity,
UnitPrice: orderItem.Price,
TotalPrice: itemTotal,
})
break
}
}
}
}
voidOrderResponse := VoidOrderResponse{
OrderID: order.ID,
Status: order.Status,
Reason: req.Reason,
VoidedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
CustomerName: order.CustomerName,
VoidedItems: voidedItems,
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: voidOrderResponse,
})
}
func (h *Handler) SplitBill(c *gin.Context) {
ctx := request.GetMyContext(c)
var req SplitBillRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
validate := validator.New()
if err := validate.Struct(req); err != nil {
response.ErrorWrapper(c, err)
return
}
var items []entity.SplitBillItem
if req.Type == "ITEM" {
items = make([]entity.SplitBillItem, len(req.Items))
for i, item := range req.Items {
items[i] = entity.SplitBillItem{
OrderItemID: item.OrderItemID,
Quantity: item.Quantity,
}
}
}
splitOrder, err := h.service.SplitBillRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Type, req.PaymentMethod, req.PaymentProvider, items, req.Amount)
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}),
})
}
@@ -6,6 +6,7 @@ import (
"enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/handlers/response"
"enaklo-pos-be/internal/services"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"net/http"
@@ -50,6 +51,7 @@ func (h *Handler) Create(c *gin.Context) {
var req request.Product
if err := c.ShouldBindJSON(&req); err != nil {
fmt.Println(err)
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
)
type TransactionDB struct {
ID string `gorm:"primaryKey;column:id"`
ID string `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"`
OrderID int64 `gorm:"column:order_id"`
Amount float64 `gorm:"column:amount"`
PaymentMethod string `gorm:"column:payment_method"`
+46
View File
@@ -41,6 +41,8 @@ type OrderRepository interface {
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, 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
}
type orderRepository struct {
@@ -979,3 +981,47 @@ func (r *orderRepository) FindByIDAndCustomerID(ctx mycontext.Context, id int64,
return order, nil
}
func (r *orderRepository) UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error {
now := time.Now()
result := r.db.Model(&models.OrderItemDB{}).
Where("order_item_id = ?", orderItemID).
Updates(map[string]interface{}{
"quantity": quantity,
"updated_at": now,
})
if result.Error != nil {
return errors.Wrap(result.Error, "failed to update order item")
}
if result.RowsAffected == 0 {
logger.ContextLogger(ctx).Warn("no order item updated")
}
return nil
}
func (r *orderRepository) UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error {
now := time.Now()
result := r.db.Model(&models.OrderDB{}).
Where("id = ?", orderID).
Updates(map[string]interface{}{
"amount": amount,
"tax": tax,
"total": total,
"updated_at": now,
})
if result.Error != nil {
return errors.Wrap(result.Error, "failed to update order totals")
}
if result.RowsAffected == 0 {
logger.ContextLogger(ctx).Warn("no order updated")
}
return nil
}
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository/models"
"github.com/google/uuid"
"github.com/pkg/errors"
"gorm.io/gorm"
)
@@ -51,7 +52,7 @@ func (r *transactionRepository) FindByOrderID(ctx mycontext.Context, orderID int
func (r *transactionRepository) toTransactionDBModel(transaction *entity.Transaction) models.TransactionDB {
return models.TransactionDB{
ID: transaction.ID,
ID: uuid.New().String(),
OrderID: transaction.OrderID,
Amount: transaction.Amount,
PaymentMethod: transaction.PaymentMethod,
@@ -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
}