Add order items
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
SplitTypeItem = "ITEM"
|
||||
SplitTypeAmount = "AMOUNT"
|
||||
|
||||
MetadataKeyLastSplitPaymentID = "last_split_payment_id"
|
||||
MetadataKeyLastSplitCustomerID = "last_split_customer_id"
|
||||
MetadataKeyLastSplitCustomerName = "last_split_customer_name"
|
||||
MetadataKeyLastSplitAmount = "last_split_amount"
|
||||
MetadataKeyLastSplitType = "last_split_type"
|
||||
MetadataKeyLastSplitQuantities = "last_split_quantities"
|
||||
)
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
type SplitBillValidation struct {
|
||||
OrderItems map[uuid.UUID]*entities.OrderItem
|
||||
PaidQuantities map[uuid.UUID]int
|
||||
Outlet *entities.Outlet
|
||||
TotalPaid float64
|
||||
RemainingBalance float64
|
||||
}
|
||||
|
||||
type SplitBillProcessor interface {
|
||||
SplitByAmount(ctx context.Context, req *models.SplitBillRequest, order *entities.Order, payment *entities.PaymentMethod, customer *entities.Customer) (*models.SplitBillResponse, error)
|
||||
SplitByItem(ctx context.Context, req *models.SplitBillRequest, order *entities.Order, payment *entities.PaymentMethod, customer *entities.Customer) (*models.SplitBillResponse, error)
|
||||
}
|
||||
|
||||
type SplitBillProcessorImpl struct {
|
||||
orderRepo OrderRepository
|
||||
orderItemRepo OrderItemRepository
|
||||
paymentRepo PaymentRepository
|
||||
paymentOrderItemRepo PaymentOrderItemRepository
|
||||
outletRepo OutletRepository
|
||||
}
|
||||
|
||||
func NewSplitBillProcessorImpl(
|
||||
orderRepo OrderRepository,
|
||||
orderItemRepo OrderItemRepository,
|
||||
paymentRepo PaymentRepository,
|
||||
paymentOrderItemRepo PaymentOrderItemRepository,
|
||||
outletRepo OutletRepository,
|
||||
) *SplitBillProcessorImpl {
|
||||
return &SplitBillProcessorImpl{
|
||||
orderRepo: orderRepo,
|
||||
orderItemRepo: orderItemRepo,
|
||||
paymentRepo: paymentRepo,
|
||||
paymentOrderItemRepo: paymentOrderItemRepo,
|
||||
outletRepo: outletRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// validateSplitBillRequest validates the split bill request and returns validation data
|
||||
func (p *SplitBillProcessorImpl) validateSplitBillRequest(ctx context.Context, req *models.SplitBillRequest, order *entities.Order) (*SplitBillValidation, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("split bill request cannot be nil")
|
||||
}
|
||||
if order == nil {
|
||||
return nil, fmt.Errorf("order cannot be nil")
|
||||
}
|
||||
if len(req.Items) == 0 && req.IsItem() {
|
||||
return nil, fmt.Errorf("split bill request must contain at least one item")
|
||||
}
|
||||
|
||||
outlet, err := p.outletRepo.GetByID(ctx, order.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found for order %s: %w", req.OrderID, err)
|
||||
}
|
||||
|
||||
totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get total paid amount for order %s: %w", req.OrderID, err)
|
||||
}
|
||||
|
||||
remainingBalance := order.TotalAmount - totalPaid
|
||||
|
||||
paidQuantities, err := p.getPaidQuantitiesForOrder(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get paid quantities for order %s: %w", req.OrderID, err)
|
||||
}
|
||||
|
||||
orderItems := make(map[uuid.UUID]*entities.OrderItem)
|
||||
for _, item := range req.Items {
|
||||
orderItem, err := p.orderItemRepo.GetByID(ctx, item.OrderItemID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order item %s not found: %w", item.OrderItemID, err)
|
||||
}
|
||||
if orderItem.OrderID != req.OrderID {
|
||||
return nil, fmt.Errorf("order item %s does not belong to order %s", item.OrderItemID, req.OrderID)
|
||||
}
|
||||
|
||||
if err := validateSplitItemQuantity(item, orderItem, paidQuantities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderItems[item.OrderItemID] = orderItem
|
||||
}
|
||||
|
||||
return &SplitBillValidation{
|
||||
OrderItems: orderItems,
|
||||
PaidQuantities: paidQuantities,
|
||||
Outlet: outlet,
|
||||
TotalPaid: totalPaid,
|
||||
RemainingBalance: remainingBalance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *SplitBillProcessorImpl) getPaidQuantitiesForOrder(ctx context.Context, orderID uuid.UUID) (map[uuid.UUID]int, error) {
|
||||
paidQuantities, err := p.paymentOrderItemRepo.GetPaidQuantitiesByOrderID(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get paid quantities: %w", err)
|
||||
}
|
||||
return paidQuantities, nil
|
||||
}
|
||||
|
||||
func validateSplitItemQuantity(item models.SplitBillItemRequest, orderItem *entities.OrderItem, paidQuantities map[uuid.UUID]int) error {
|
||||
if item.Quantity <= 0 {
|
||||
return fmt.Errorf("requested quantity must be greater than 0 for order item %s", item.OrderItemID)
|
||||
}
|
||||
alreadyPaid := paidQuantities[item.OrderItemID]
|
||||
availableQuantity := orderItem.Quantity - alreadyPaid
|
||||
if item.Quantity > availableQuantity {
|
||||
return fmt.Errorf("requested quantity %d for order item %s exceeds available quantity %d (total: %d, already paid: %d)", item.Quantity, item.OrderItemID, availableQuantity, orderItem.Quantity, alreadyPaid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateSplitAmounts(req *models.SplitBillRequest, validation *SplitBillValidation) (float64, float64, []models.SplitBillItemResponse) {
|
||||
var totalSplitAmount float64
|
||||
var responseItems []models.SplitBillItemResponse
|
||||
|
||||
for _, item := range req.Items {
|
||||
orderItem := validation.OrderItems[item.OrderItemID]
|
||||
itemAmount := float64(item.Quantity) * orderItem.UnitPrice
|
||||
itemTaxAmount := itemAmount * validation.Outlet.TaxRate
|
||||
totalItemAmount := itemAmount + itemTaxAmount
|
||||
|
||||
totalSplitAmount += itemAmount
|
||||
|
||||
responseItems = append(responseItems, models.SplitBillItemResponse{
|
||||
OrderItemID: item.OrderItemID,
|
||||
Amount: totalItemAmount,
|
||||
})
|
||||
}
|
||||
|
||||
splitTaxAmount := totalSplitAmount * validation.Outlet.TaxRate
|
||||
totalSplitAmountWithTax := totalSplitAmount + splitTaxAmount
|
||||
return totalSplitAmount, totalSplitAmountWithTax, responseItems
|
||||
}
|
||||
|
||||
func (p *SplitBillProcessorImpl) createSplitPayment(ctx context.Context, req *models.SplitBillRequest, order *entities.Order, payment *entities.PaymentMethod, customer *entities.Customer, totalSplitAmountWithTax float64, existingPayments []*entities.Payment) (*entities.Payment, error) {
|
||||
splitNumber := len(existingPayments) + 1
|
||||
splitTotal := splitNumber + 1
|
||||
splitType := entities.SplitTypeItem
|
||||
|
||||
splitPayment := &entities.Payment{
|
||||
OrderID: req.OrderID,
|
||||
PaymentMethodID: payment.ID,
|
||||
Amount: totalSplitAmountWithTax,
|
||||
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": SplitTypeItem,
|
||||
"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)
|
||||
}
|
||||
return splitPayment, nil
|
||||
}
|
||||
|
||||
func (p *SplitBillProcessorImpl) createPaymentOrderItems(ctx context.Context, splitPayment *entities.Payment, req *models.SplitBillRequest, validation *SplitBillValidation) error {
|
||||
for _, item := range req.Items {
|
||||
orderItem := validation.OrderItems[item.OrderItemID]
|
||||
itemAmount := float64(item.Quantity) * orderItem.UnitPrice
|
||||
itemTaxAmount := itemAmount * validation.Outlet.TaxRate
|
||||
totalItemAmount := itemAmount + itemTaxAmount
|
||||
|
||||
paymentOrderItem := &entities.PaymentOrderItem{
|
||||
PaymentID: splitPayment.ID,
|
||||
OrderItemID: item.OrderItemID,
|
||||
Quantity: item.Quantity,
|
||||
Amount: totalItemAmount,
|
||||
}
|
||||
if err := p.paymentOrderItemRepo.Create(ctx, paymentOrderItem); err != nil {
|
||||
return fmt.Errorf("failed to create payment order item: %w", err)
|
||||
}
|
||||
|
||||
// Update order item status to paid if fully covered
|
||||
alreadyPaid := validation.PaidQuantities[item.OrderItemID]
|
||||
newPaid := alreadyPaid + item.Quantity
|
||||
if newPaid >= orderItem.Quantity {
|
||||
if err := p.orderItemRepo.UpdateStatus(ctx, item.OrderItemID, entities.OrderItemStatusPaid); err != nil {
|
||||
return fmt.Errorf("failed to update order item status to paid: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SplitBillProcessorImpl) updateOrderAfterSplit(ctx context.Context, order *entities.Order, req *models.SplitBillRequest, customer *entities.Customer, totalSplitAmountWithTax float64, validation *SplitBillValidation) error {
|
||||
if order.Metadata == nil {
|
||||
order.Metadata = make(entities.Metadata)
|
||||
}
|
||||
order.Metadata[MetadataKeyLastSplitPaymentID] = req.OrderID.String()
|
||||
order.Metadata[MetadataKeyLastSplitCustomerID] = req.CustomerID.String()
|
||||
order.Metadata[MetadataKeyLastSplitCustomerName] = customer.Name
|
||||
order.Metadata[MetadataKeyLastSplitAmount] = totalSplitAmountWithTax
|
||||
order.Metadata[MetadataKeyLastSplitType] = SplitTypeItem
|
||||
|
||||
quantityInfo := make(map[string]interface{})
|
||||
for _, item := range req.Items {
|
||||
orderItem := validation.OrderItems[item.OrderItemID]
|
||||
quantityInfo[item.OrderItemID.String()] = map[string]interface{}{
|
||||
"quantity": item.Quantity,
|
||||
"unit_price": orderItem.UnitPrice,
|
||||
"total_amount": float64(item.Quantity) * orderItem.UnitPrice,
|
||||
}
|
||||
}
|
||||
order.Metadata[MetadataKeyLastSplitQuantities] = quantityInfo
|
||||
|
||||
newTotalPaid := validation.TotalPaid + totalSplitAmountWithTax
|
||||
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 fmt.Errorf("failed to update order: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SplitBillProcessorImpl) SplitByItem(ctx context.Context, req *models.SplitBillRequest, order *entities.Order, payment *entities.PaymentMethod, customer *entities.Customer) (*models.SplitBillResponse, error) {
|
||||
validation, err := p.validateSplitBillRequest(ctx, req, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, totalSplitAmountWithTax, responseItems := calculateSplitAmounts(req, validation)
|
||||
if totalSplitAmountWithTax > validation.RemainingBalance {
|
||||
return nil, fmt.Errorf("split amount %.2f cannot exceed remaining balance %.2f", totalSplitAmountWithTax, validation.RemainingBalance)
|
||||
}
|
||||
existingPayments, err := p.paymentRepo.GetByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get existing payments: %w", err)
|
||||
}
|
||||
splitPayment, err := p.createSplitPayment(ctx, req, order, payment, customer, totalSplitAmountWithTax, existingPayments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.createPaymentOrderItems(ctx, splitPayment, req, validation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.updateOrderAfterSplit(ctx, order, req, customer, totalSplitAmountWithTax, validation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &models.SplitBillResponse{
|
||||
PaymentID: splitPayment.ID,
|
||||
OrderID: req.OrderID,
|
||||
CustomerID: req.CustomerID,
|
||||
Type: SplitTypeItem,
|
||||
Amount: totalSplitAmountWithTax,
|
||||
Items: responseItems,
|
||||
Message: fmt.Sprintf("Successfully split payment by items (%.2f) for customer %s. Remaining balance: %.2f", totalSplitAmountWithTax, customer.Name, order.RemainingAmount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *SplitBillProcessorImpl) SplitByAmount(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": SplitTypeAmount,
|
||||
},
|
||||
}
|
||||
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[MetadataKeyLastSplitPaymentID] = splitPayment.ID.String()
|
||||
order.Metadata[MetadataKeyLastSplitCustomerID] = req.CustomerID.String()
|
||||
order.Metadata[MetadataKeyLastSplitAmount] = req.Amount
|
||||
order.Metadata[MetadataKeyLastSplitType] = SplitTypeAmount
|
||||
|
||||
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: SplitTypeAmount,
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user