diff --git a/internal/constants/order.go b/internal/constants/order.go index ba359d9..2982419 100644 --- a/internal/constants/order.go +++ b/internal/constants/order.go @@ -28,6 +28,7 @@ const ( OrderItemStatusServed OrderItemStatus = "served" OrderItemStatusCancelled OrderItemStatus = "cancelled" OrderItemStatusCompleted OrderItemStatus = "completed" + OrderItemStatusPaid OrderItemStatus = "paid" ) func GetAllOrderTypes() []OrderType { diff --git a/internal/contract/order_contract.go b/internal/contract/order_contract.go index b2e0d93..59fa1ae 100644 --- a/internal/contract/order_contract.go +++ b/internal/contract/order_contract.go @@ -108,6 +108,7 @@ type OrderItemResponse struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` PrinterType string `json:"printer_type"` + PaidQuantity int `json:"paid_quantity"` } type ListOrdersQuery struct { @@ -249,7 +250,7 @@ type SplitBillRequest struct { type SplitBillItemRequest struct { OrderItemID uuid.UUID `json:"order_item_id" validate:"required"` - Amount float64 `json:"amount" validate:"required,min=0"` + Quantity int `json:"quantity" validate:"required,min=0"` } type SplitBillResponse struct { diff --git a/internal/entities/order_item.go b/internal/entities/order_item.go index 14a708c..72cbf58 100644 --- a/internal/entities/order_item.go +++ b/internal/entities/order_item.go @@ -38,6 +38,7 @@ const ( OrderItemStatusReady OrderItemStatus = "ready" OrderItemStatusServed OrderItemStatus = "served" OrderItemStatusCancelled OrderItemStatus = "cancelled" + OrderItemStatusPaid OrderItemStatus = "paid" ) type OrderItem struct { diff --git a/internal/entities/payment_order_item.go b/internal/entities/payment_order_item.go index ee78b98..acaaa1b 100644 --- a/internal/entities/payment_order_item.go +++ b/internal/entities/payment_order_item.go @@ -11,6 +11,7 @@ type PaymentOrderItem struct { ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` PaymentID uuid.UUID `gorm:"type:uuid;not null;index" json:"payment_id"` OrderItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"order_item_id"` + Quantity int `gorm:"not null;default:0" json:"quantity"` // Quantity paid for this specific payment Amount float64 `gorm:"type:decimal(10,2);not null" json:"amount"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` diff --git a/internal/mappers/order_mapper.go b/internal/mappers/order_mapper.go index 059adaa..5f3c4da 100644 --- a/internal/mappers/order_mapper.go +++ b/internal/mappers/order_mapper.go @@ -70,8 +70,23 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse { // Map order items if order.OrderItems != nil { response.OrderItems = make([]models.OrderItemResponse, len(order.OrderItems)) + + // Build map of paid quantities per order item from payments + paidQtyByOrderItem := map[uuid.UUID]int{} + if order.Payments != nil { + for _, p := range order.Payments { + for _, poi := range p.PaymentOrderItems { + paidQtyByOrderItem[poi.OrderItemID] += poi.Quantity + } + } + } + for i, item := range order.OrderItems { - response.OrderItems[i] = *OrderItemEntityToResponse(&item) + resp := OrderItemEntityToResponse(&item) + if resp != nil { + resp.PaidQuantity = paidQtyByOrderItem[item.ID] + response.OrderItems[i] = *resp + } } } diff --git a/internal/models/order.go b/internal/models/order.go index 5aabcac..39fddea 100644 --- a/internal/models/order.go +++ b/internal/models/order.go @@ -65,6 +65,7 @@ type PaymentOrderItem struct { ID uuid.UUID PaymentID uuid.UUID OrderItemID uuid.UUID + Quantity int // Quantity paid for this specific payment Amount float64 CreatedAt time.Time UpdatedAt time.Time @@ -114,6 +115,7 @@ type UpdateOrderRequest struct { type CreatePaymentOrderItemRequest struct { OrderItemID uuid.UUID `validate:"required"` + Quantity int `validate:"required,min=1"` // Quantity being paid for Amount float64 `validate:"required,min=0"` } @@ -205,12 +207,14 @@ type OrderItemResponse struct { CreatedAt time.Time UpdatedAt time.Time PrinterType string + PaidQuantity int } type PaymentOrderItemResponse struct { ID uuid.UUID PaymentID uuid.UUID OrderItemID uuid.UUID + Quantity int // Quantity paid for this specific payment Amount float64 CreatedAt time.Time UpdatedAt time.Time diff --git a/internal/models/split_bill.go b/internal/models/split_bill.go index 7982e23..54ac78f 100644 --- a/internal/models/split_bill.go +++ b/internal/models/split_bill.go @@ -27,7 +27,7 @@ func (s *SplitBillRequest) IsAmount() bool { type SplitBillItemRequest struct { OrderItemID uuid.UUID `validate:"required"` - Amount float64 `validate:"required,min=0"` + Quantity int `validate:"required,min=0"` } type SplitBillResponse struct { diff --git a/internal/processor/order_processor.go b/internal/processor/order_processor.go index 5625e31..7e2082b 100644 --- a/internal/processor/order_processor.go +++ b/internal/processor/order_processor.go @@ -74,6 +74,7 @@ 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) + GetPaidQuantitiesByOrderID(ctx context.Context, orderID uuid.UUID) (map[uuid.UUID]int, error) Update(ctx context.Context, paymentOrderItem *entities.PaymentOrderItem) error Delete(ctx context.Context, id uuid.UUID) error } @@ -110,6 +111,7 @@ type OrderProcessorImpl struct { productVariantRepo repository.ProductVariantRepository outletRepo OutletRepository customerRepo CustomerRepository + splitBillProcessor SplitBillProcessor } func NewOrderProcessorImpl( @@ -137,6 +139,7 @@ func NewOrderProcessorImpl( productVariantRepo: productVariantRepo, outletRepo: outletRepo, customerRepo: customerRepo, + splitBillProcessor: NewSplitBillProcessorImpl(orderRepo, orderItemRepo, paymentRepo, paymentOrderItemRepo, outletRepo), } } @@ -800,10 +803,6 @@ func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.Crea return response, nil } -func stringPtr(s string) *string { - return &s -} - func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error { payment, err := p.paymentRepo.GetByID(ctx, paymentID) if err != nil { @@ -910,200 +909,14 @@ func (p *OrderProcessorImpl) SplitBill(ctx context.Context, req *models.SplitBil var response *models.SplitBillResponse if req.IsAmount() { - response, err = p.splitBillByAmount(ctx, req, order, payment, customer) + response, err = p.splitBillProcessor.SplitByAmount(ctx, req, order, payment, customer) } else if req.IsItem() { - response, err = p.splitBillByItem(ctx, req, order, payment, customer) + response, err = p.splitBillProcessor.SplitByItem(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 -} diff --git a/internal/processor/split_bill_processor.go b/internal/processor/split_bill_processor.go new file mode 100644 index 0000000..d5e4f19 --- /dev/null +++ b/internal/processor/split_bill_processor.go @@ -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 +} diff --git a/internal/repository/payment_order_item_repository.go b/internal/repository/payment_order_item_repository.go index 354db87..7b11255 100644 --- a/internal/repository/payment_order_item_repository.go +++ b/internal/repository/payment_order_item_repository.go @@ -13,6 +13,7 @@ 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) + GetPaidQuantitiesByOrderID(ctx context.Context, orderID uuid.UUID) (map[uuid.UUID]int, error) Update(ctx context.Context, paymentOrderItem *entities.PaymentOrderItem) error Delete(ctx context.Context, id uuid.UUID) error } @@ -60,3 +61,34 @@ func (r *PaymentOrderItemRepositoryImpl) Update(ctx context.Context, paymentOrde func (r *PaymentOrderItemRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { return r.db.WithContext(ctx).Delete(&entities.PaymentOrderItem{}, "id = ?", id).Error } + +// GetPaidQuantitiesByOrderID efficiently aggregates paid quantities for an order using SQL +func (r *PaymentOrderItemRepositoryImpl) GetPaidQuantitiesByOrderID(ctx context.Context, orderID uuid.UUID) (map[uuid.UUID]int, error) { + type Result struct { + OrderItemID uuid.UUID `json:"order_item_id"` + TotalQuantity int `json:"total_quantity"` + } + + var results []Result + + // Efficient SQL query that aggregates quantities in the database + err := r.db.WithContext(ctx). + Table("payment_order_items poi"). + Select("poi.order_item_id, SUM(poi.quantity) as total_quantity"). + Joins("JOIN payments p ON poi.payment_id = p.id"). + Where("p.order_id = ?", orderID). + Group("poi.order_item_id"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + // Convert results to map + paidQuantities := make(map[uuid.UUID]int) + for _, result := range results { + paidQuantities[result.OrderItemID] = result.TotalQuantity + } + + return paidQuantities, nil +} diff --git a/internal/service/order_service.go b/internal/service/order_service.go index a4da84b..05a84a3 100644 --- a/internal/service/order_service.go +++ b/internal/service/order_service.go @@ -449,17 +449,17 @@ func (s *OrderServiceImpl) validateSplitBillRequest(req *models.SplitBillRequest return fmt.Errorf("items are required when splitting by ITEM") } - totalItemAmount := float64(0) + totalItemAmount := 0 for i, item := range req.Items { if item.OrderItemID == uuid.Nil { return fmt.Errorf("order item ID is required for item %d", i+1) } - if item.Amount <= 0 { - return fmt.Errorf("amount must be greater than zero for item %d", i+1) + if item.Quantity <= 0 { + return fmt.Errorf("quantity must be greater than zero for item %d", i+1) } - totalItemAmount += item.Amount + totalItemAmount += item.Quantity } if totalItemAmount <= 0 { diff --git a/internal/transformer/order_transformer.go b/internal/transformer/order_transformer.go index 7b407cf..a7975c9 100644 --- a/internal/transformer/order_transformer.go +++ b/internal/transformer/order_transformer.go @@ -110,6 +110,7 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse { CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, PrinterType: item.PrinterType, + PaidQuantity: item.PaidQuantity, } } // Map payments @@ -379,7 +380,7 @@ func SplitBillContractToModel(req *contract.SplitBillRequest) *models.SplitBillR for i, item := range req.Items { items[i] = models.SplitBillItemRequest{ OrderItemID: item.OrderItemID, - Amount: item.Amount, + Quantity: item.Quantity, } } return &models.SplitBillRequest{ diff --git a/migrations/000037_add_quantity_to_payment_order_items.down.sql b/migrations/000037_add_quantity_to_payment_order_items.down.sql new file mode 100644 index 0000000..5a8040f --- /dev/null +++ b/migrations/000037_add_quantity_to_payment_order_items.down.sql @@ -0,0 +1,3 @@ +-- Remove quantity field from payment_order_items table +ALTER TABLE payment_order_items +DROP COLUMN quantity; \ No newline at end of file diff --git a/migrations/000037_add_quantity_to_payment_order_items.up.sql b/migrations/000037_add_quantity_to_payment_order_items.up.sql new file mode 100644 index 0000000..a77120d --- /dev/null +++ b/migrations/000037_add_quantity_to_payment_order_items.up.sql @@ -0,0 +1,6 @@ +-- Add quantity field to payment_order_items table +ALTER TABLE payment_order_items +ADD COLUMN quantity INTEGER NOT NULL DEFAULT 0; + +-- Add comment to explain the field +COMMENT ON COLUMN payment_order_items.quantity IS 'Quantity paid for this specific payment split'; \ No newline at end of file