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 }