Fix Split Bill
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
@@ -24,6 +26,7 @@ type OrderProcessor interface {
|
||||
CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error)
|
||||
RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
||||
SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error)
|
||||
SplitBill(ctx context.Context, req *models.SplitBillRequest) (*models.SplitBillResponse, error)
|
||||
}
|
||||
|
||||
type OrderRepository interface {
|
||||
@@ -67,6 +70,14 @@ type PaymentRepository interface {
|
||||
RefundPaymentWithInventoryMovement(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID, order *entities.Payment) error
|
||||
}
|
||||
|
||||
type PaymentOrderItemRepository interface {
|
||||
Create(ctx context.Context, paymentOrderItem *entities.PaymentOrderItem) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentOrderItem, error)
|
||||
GetByPaymentID(ctx context.Context, paymentID uuid.UUID) ([]*entities.PaymentOrderItem, error)
|
||||
Update(ctx context.Context, paymentOrderItem *entities.PaymentOrderItem) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
type PaymentMethodRepository interface {
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error)
|
||||
}
|
||||
@@ -95,6 +106,7 @@ type OrderProcessorImpl struct {
|
||||
orderRepo OrderRepository
|
||||
orderItemRepo OrderItemRepository
|
||||
paymentRepo PaymentRepository
|
||||
paymentOrderItemRepo PaymentOrderItemRepository
|
||||
productRepo ProductRepository
|
||||
paymentMethodRepo PaymentMethodRepository
|
||||
inventoryRepo repository.InventoryRepository
|
||||
@@ -108,6 +120,7 @@ func NewOrderProcessorImpl(
|
||||
orderRepo OrderRepository,
|
||||
orderItemRepo OrderItemRepository,
|
||||
paymentRepo PaymentRepository,
|
||||
paymentOrderItemRepo PaymentOrderItemRepository,
|
||||
productRepo ProductRepository,
|
||||
paymentMethodRepo PaymentMethodRepository,
|
||||
inventoryRepo repository.InventoryRepository,
|
||||
@@ -120,6 +133,7 @@ func NewOrderProcessorImpl(
|
||||
orderRepo: orderRepo,
|
||||
orderItemRepo: orderItemRepo,
|
||||
paymentRepo: paymentRepo,
|
||||
paymentOrderItemRepo: paymentOrderItemRepo,
|
||||
productRepo: productRepo,
|
||||
paymentMethodRepo: paymentMethodRepo,
|
||||
inventoryRepo: inventoryRepo,
|
||||
@@ -203,23 +217,24 @@ func (p *OrderProcessorImpl) CreateOrder(ctx context.Context, req *models.Create
|
||||
metadata["customer_name"] = *req.CustomerName
|
||||
}
|
||||
order := &entities.Order{
|
||||
OrganizationID: organizationID,
|
||||
OutletID: req.OutletID,
|
||||
UserID: req.UserID,
|
||||
CustomerID: req.CustomerID,
|
||||
OrderNumber: orderNumber,
|
||||
TableNumber: req.TableNumber,
|
||||
OrderType: entities.OrderType(req.OrderType),
|
||||
Status: entities.OrderStatusPending,
|
||||
Subtotal: subtotal,
|
||||
TaxAmount: taxAmount,
|
||||
DiscountAmount: 0,
|
||||
TotalAmount: totalAmount,
|
||||
TotalCost: totalCost,
|
||||
PaymentStatus: entities.PaymentStatusPending,
|
||||
IsVoid: false,
|
||||
IsRefund: false,
|
||||
Metadata: metadata,
|
||||
OrganizationID: organizationID,
|
||||
OutletID: req.OutletID,
|
||||
UserID: req.UserID,
|
||||
CustomerID: req.CustomerID,
|
||||
OrderNumber: orderNumber,
|
||||
TableNumber: req.TableNumber,
|
||||
OrderType: entities.OrderType(req.OrderType),
|
||||
Status: entities.OrderStatusPending,
|
||||
Subtotal: subtotal,
|
||||
TaxAmount: taxAmount,
|
||||
DiscountAmount: 0,
|
||||
TotalAmount: totalAmount,
|
||||
TotalCost: totalCost,
|
||||
RemainingAmount: totalAmount, // Initialize remaining amount equal to total amount
|
||||
PaymentStatus: entities.PaymentStatusPending,
|
||||
IsVoid: false,
|
||||
IsRefund: false,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
if err := p.orderRepo.Create(ctx, order); err != nil {
|
||||
@@ -325,6 +340,17 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
||||
order.TaxAmount = order.Subtotal * outlet.TaxRate
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
|
||||
// Recalculate remaining amount when items are added
|
||||
totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get total paid amount: %w", err)
|
||||
}
|
||||
|
||||
order.RemainingAmount = order.TotalAmount - totalPaid
|
||||
if order.RemainingAmount < 0 {
|
||||
order.RemainingAmount = 0
|
||||
}
|
||||
|
||||
if req.Metadata != nil {
|
||||
if order.Metadata == nil {
|
||||
order.Metadata = make(entities.Metadata)
|
||||
@@ -409,6 +435,17 @@ func (p *OrderProcessorImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req
|
||||
order.DiscountAmount = *req.DiscountAmount
|
||||
// Recalculate total amount
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
|
||||
// Recalculate remaining amount when discount is applied
|
||||
totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get total paid amount: %w", err)
|
||||
}
|
||||
|
||||
order.RemainingAmount = order.TotalAmount - totalPaid
|
||||
if order.RemainingAmount < 0 {
|
||||
order.RemainingAmount = 0
|
||||
}
|
||||
}
|
||||
if req.Metadata != nil {
|
||||
if order.Metadata == nil {
|
||||
@@ -489,16 +526,20 @@ func (p *OrderProcessorImpl) ListOrders(ctx context.Context, req *models.ListOrd
|
||||
return nil, fmt.Errorf("failed to list orders: %w", err)
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
orderResponses := make([]models.OrderResponse, len(orders))
|
||||
allPayments := make([]models.PaymentResponse, 0)
|
||||
|
||||
for i, order := range orders {
|
||||
response := mappers.OrderEntityToResponse(order)
|
||||
if response != nil {
|
||||
orderResponses[i] = *response
|
||||
// Add payments from this order to the allPayments list
|
||||
if response.Payments != nil {
|
||||
allPayments = append(allPayments, response.Payments...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total pages
|
||||
totalPages := int(total) / req.Limit
|
||||
if int(total)%req.Limit > 0 {
|
||||
totalPages++
|
||||
@@ -506,6 +547,7 @@ func (p *OrderProcessorImpl) ListOrders(ctx context.Context, req *models.ListOrd
|
||||
|
||||
return &models.ListOrdersResponse{
|
||||
Orders: orderResponses,
|
||||
Payments: allPayments,
|
||||
TotalCount: int(total),
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
@@ -738,6 +780,21 @@ func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.Crea
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update order payment status and remaining amount in processor layer
|
||||
newTotalPaid := totalPaid + req.Amount
|
||||
order.RemainingAmount = order.TotalAmount - newTotalPaid
|
||||
|
||||
if newTotalPaid >= order.TotalAmount {
|
||||
order.PaymentStatus = entities.PaymentStatusCompleted
|
||||
order.RemainingAmount = 0
|
||||
} else {
|
||||
order.PaymentStatus = entities.PaymentStatusPartial
|
||||
}
|
||||
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order payment status: %w", err)
|
||||
}
|
||||
|
||||
paymentWithRelations, err := p.paymentRepo.GetByID(ctx, payment.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve created payment: %w", err)
|
||||
@@ -769,18 +826,15 @@ func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.U
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) {
|
||||
// Get the order
|
||||
order, err := p.orderRepo.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify order belongs to the organization
|
||||
if order.OrganizationID != organizationID {
|
||||
return nil, fmt.Errorf("order does not belong to the organization")
|
||||
}
|
||||
|
||||
// Check if order status is pending (only pending orders can have customer set)
|
||||
if order.Status != entities.OrderStatusPending {
|
||||
return nil, fmt.Errorf("customer can only be set for pending orders")
|
||||
}
|
||||
@@ -805,3 +859,255 @@ func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) SplitBill(ctx context.Context, req *models.SplitBillRequest) (*models.SplitBillResponse, error) {
|
||||
order, err := p.orderRepo.GetWithRelations(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
if order.IsVoid {
|
||||
return nil, fmt.Errorf("cannot split voided order")
|
||||
}
|
||||
|
||||
if order.PaymentStatus == entities.PaymentStatusCompleted {
|
||||
return nil, fmt.Errorf("cannot split fully paid order")
|
||||
}
|
||||
|
||||
existingPayments, err := p.paymentRepo.GetByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get existing payments: %w", err)
|
||||
}
|
||||
|
||||
var existingSplitType *entities.SplitType
|
||||
for _, payment := range existingPayments {
|
||||
if payment.SplitType != nil && payment.SplitTotal > 1 {
|
||||
existingSplitType = payment.SplitType
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existingSplitType != nil {
|
||||
requestedSplitType := entities.SplitTypeAmount
|
||||
if req.IsItem() {
|
||||
requestedSplitType = entities.SplitTypeItem
|
||||
}
|
||||
|
||||
if *existingSplitType != requestedSplitType {
|
||||
return nil, fmt.Errorf("order already has %s split payments. Subsequent payments must use the same split type", *existingSplitType)
|
||||
}
|
||||
}
|
||||
|
||||
payment, err := p.paymentMethodRepo.GetByID(ctx, req.PaymentMethodID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("payment method not found: %w", err)
|
||||
}
|
||||
|
||||
customer := &entities.Customer{}
|
||||
if req.CustomerID != uuid.Nil {
|
||||
customer, err = p.customerRepo.GetByIDAndOrganization(ctx, req.CustomerID, order.OrganizationID)
|
||||
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("customer not found or does not belong to the organization: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var response *models.SplitBillResponse
|
||||
if req.IsAmount() {
|
||||
response, err = p.splitBillByAmount(ctx, req, order, payment, customer)
|
||||
} else if req.IsItem() {
|
||||
response, err = p.splitBillByItem(ctx, req, order, payment, customer)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid split type: must be AMOUNT or ITEM")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) splitBillByAmount(ctx context.Context, req *models.SplitBillRequest, order *entities.Order, payment *entities.PaymentMethod, customer *entities.Customer) (*models.SplitBillResponse, error) {
|
||||
totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get total paid amount: %w", err)
|
||||
}
|
||||
|
||||
remainingBalance := order.TotalAmount - totalPaid
|
||||
if req.Amount > remainingBalance {
|
||||
return nil, fmt.Errorf("split amount %.2f cannot exceed remaining balance %.2f", req.Amount, remainingBalance)
|
||||
}
|
||||
|
||||
existingPayments, err := p.paymentRepo.GetByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get existing payments: %w", err)
|
||||
}
|
||||
|
||||
splitNumber := len(existingPayments) + 1
|
||||
splitTotal := splitNumber + 1
|
||||
|
||||
splitType := entities.SplitTypeAmount
|
||||
splitPayment := &entities.Payment{
|
||||
OrderID: req.OrderID,
|
||||
PaymentMethodID: payment.ID,
|
||||
Amount: req.Amount,
|
||||
Status: entities.PaymentTransactionStatusCompleted,
|
||||
SplitNumber: splitNumber,
|
||||
SplitTotal: splitTotal,
|
||||
SplitType: &splitType,
|
||||
SplitDescription: stringPtr(fmt.Sprint("Split payment for customer")),
|
||||
Metadata: entities.Metadata{
|
||||
"split_type": "AMOUNT",
|
||||
},
|
||||
}
|
||||
|
||||
if err := p.paymentRepo.Create(ctx, splitPayment); err != nil {
|
||||
return nil, fmt.Errorf("failed to create split payment: %w", err)
|
||||
}
|
||||
|
||||
if order.Metadata == nil {
|
||||
order.Metadata = make(entities.Metadata)
|
||||
}
|
||||
|
||||
order.Metadata["last_split_payment_id"] = splitPayment.ID.String()
|
||||
order.Metadata["last_split_customer_id"] = req.CustomerID.String()
|
||||
order.Metadata["last_split_amount"] = req.Amount
|
||||
order.Metadata["last_split_type"] = "AMOUNT"
|
||||
|
||||
newTotalPaid := totalPaid + req.Amount
|
||||
order.RemainingAmount = order.TotalAmount - newTotalPaid
|
||||
|
||||
if newTotalPaid >= order.TotalAmount {
|
||||
order.PaymentStatus = entities.PaymentStatusCompleted
|
||||
order.Status = entities.OrderStatusCompleted
|
||||
order.RemainingAmount = 0
|
||||
} else {
|
||||
order.PaymentStatus = entities.PaymentStatusPartial
|
||||
}
|
||||
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||
}
|
||||
|
||||
return &models.SplitBillResponse{
|
||||
PaymentID: splitPayment.ID,
|
||||
OrderID: req.OrderID,
|
||||
CustomerID: req.CustomerID,
|
||||
Type: "AMOUNT",
|
||||
Amount: req.Amount,
|
||||
Message: fmt.Sprintf("Successfully split payment by amount %.2f for customer %s. Remaining balance: %.2f", req.Amount, customer.Name, order.RemainingAmount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) splitBillByItem(ctx context.Context, req *models.SplitBillRequest, order *entities.Order, payment *entities.PaymentMethod, customer *entities.Customer) (*models.SplitBillResponse, error) {
|
||||
totalSplitAmount := float64(0)
|
||||
for _, item := range req.Items {
|
||||
totalSplitAmount += item.Amount
|
||||
}
|
||||
|
||||
totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get total paid amount: %w", err)
|
||||
}
|
||||
|
||||
remainingBalance := order.TotalAmount - totalPaid
|
||||
|
||||
if totalSplitAmount > remainingBalance {
|
||||
return nil, fmt.Errorf("split amount %.2f cannot exceed remaining balance %.2f", totalSplitAmount, remainingBalance)
|
||||
}
|
||||
|
||||
for _, item := range req.Items {
|
||||
orderItem, err := p.orderItemRepo.GetByID(ctx, item.OrderItemID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order item not found: %w", err)
|
||||
}
|
||||
if orderItem.OrderID != req.OrderID {
|
||||
return nil, fmt.Errorf("order item does not belong to this order")
|
||||
}
|
||||
}
|
||||
|
||||
existingPayments, err := p.paymentRepo.GetByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get existing payments: %w", err)
|
||||
}
|
||||
|
||||
splitNumber := len(existingPayments) + 1
|
||||
splitTotal := splitNumber + 1
|
||||
|
||||
splitType := entities.SplitTypeItem
|
||||
splitPayment := &entities.Payment{
|
||||
OrderID: req.OrderID,
|
||||
PaymentMethodID: payment.ID,
|
||||
Amount: totalSplitAmount,
|
||||
Status: entities.PaymentTransactionStatusCompleted,
|
||||
SplitNumber: splitNumber,
|
||||
SplitTotal: splitTotal,
|
||||
SplitType: &splitType,
|
||||
SplitDescription: stringPtr(fmt.Sprintf("Split payment by items for customer: %s", customer.Name)),
|
||||
Metadata: entities.Metadata{
|
||||
"split_type": "ITEM",
|
||||
"customer_id": req.CustomerID.String(),
|
||||
"customer_name": customer.Name,
|
||||
},
|
||||
}
|
||||
|
||||
if err := p.paymentRepo.Create(ctx, splitPayment); err != nil {
|
||||
return nil, fmt.Errorf("failed to create split payment: %w", err)
|
||||
}
|
||||
|
||||
for _, item := range req.Items {
|
||||
paymentOrderItem := &entities.PaymentOrderItem{
|
||||
PaymentID: splitPayment.ID,
|
||||
OrderItemID: item.OrderItemID,
|
||||
Amount: item.Amount,
|
||||
}
|
||||
|
||||
if err := p.paymentOrderItemRepo.Create(ctx, paymentOrderItem); err != nil {
|
||||
return nil, fmt.Errorf("failed to create payment order item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if order.Metadata == nil {
|
||||
order.Metadata = make(entities.Metadata)
|
||||
}
|
||||
|
||||
order.Metadata["last_split_payment_id"] = splitPayment.ID.String()
|
||||
order.Metadata["last_split_customer_id"] = req.CustomerID.String()
|
||||
order.Metadata["last_split_customer_name"] = customer.Name
|
||||
order.Metadata["last_split_amount"] = totalSplitAmount
|
||||
order.Metadata["last_split_type"] = "ITEM"
|
||||
|
||||
newTotalPaid := totalPaid + totalSplitAmount
|
||||
order.RemainingAmount = order.TotalAmount - newTotalPaid
|
||||
|
||||
if newTotalPaid >= order.TotalAmount {
|
||||
order.PaymentStatus = entities.PaymentStatusCompleted
|
||||
order.Status = entities.OrderStatusCompleted
|
||||
order.RemainingAmount = 0
|
||||
} else {
|
||||
order.PaymentStatus = entities.PaymentStatusPartial
|
||||
}
|
||||
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||
}
|
||||
|
||||
responseItems := make([]models.SplitBillItemResponse, len(req.Items))
|
||||
for i, item := range req.Items {
|
||||
responseItems[i] = models.SplitBillItemResponse{
|
||||
OrderItemID: item.OrderItemID,
|
||||
Amount: item.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.SplitBillResponse{
|
||||
PaymentID: splitPayment.ID,
|
||||
OrderID: req.OrderID,
|
||||
CustomerID: req.CustomerID,
|
||||
Type: "ITEM",
|
||||
Amount: totalSplitAmount,
|
||||
Items: responseItems,
|
||||
Message: fmt.Sprintf("Successfully split payment by items (%.2f) for customer %s. Remaining balance: %.2f", totalSplitAmount, customer.Name, order.RemainingAmount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -31,10 +31,8 @@ func NewOutletProcessorImpl(outletRepo *repository.OutletRepositoryImpl) *Outlet
|
||||
}
|
||||
|
||||
func (p *OutletProcessorImpl) ListOutletsByOrganization(ctx context.Context, organizationID uuid.UUID, page, limit int) ([]*models.OutletResponse, int64, error) {
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Get outlets with pagination
|
||||
outlets, total, err := p.outletRepo.GetByOrganizationIDWithPagination(ctx, organizationID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get outlets: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user