package processor import ( "context" "fmt" "gorm.io/gorm" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/mappers" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" "github.com/google/uuid" ) type OrderProcessor interface { CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error 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 { Create(ctx context.Context, order *entities.Order) error GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error) Update(ctx context.Context, order *entities.Order) error Delete(ctx context.Context, id uuid.UUID) error List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error) GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error) ExistsByOrderNumber(ctx context.Context, orderNumber string) (bool, error) VoidOrder(ctx context.Context, id uuid.UUID, reason string, voidedBy uuid.UUID) error VoidOrderWithStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus, reason string, voidedBy uuid.UUID) error RefundOrder(ctx context.Context, id uuid.UUID, reason string, refundedBy uuid.UUID) error UpdatePaymentStatus(ctx context.Context, id uuid.UUID, status entities.PaymentStatus) error UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus) error GetNextOrderNumber(ctx context.Context, organizationID, outletID uuid.UUID) (string, error) } type OrderItemRepository interface { Create(ctx context.Context, orderItem *entities.OrderItem) error GetByID(ctx context.Context, id uuid.UUID) (*entities.OrderItem, error) GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.OrderItem, error) Update(ctx context.Context, orderItem *entities.OrderItem) error Delete(ctx context.Context, id uuid.UUID) error RefundOrderItem(ctx context.Context, id uuid.UUID, refundQuantity int, refundAmount float64, reason string, refundedBy uuid.UUID) error VoidOrderItem(ctx context.Context, id uuid.UUID, voidQuantity int, reason string, voidedBy uuid.UUID) error UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderItemStatus) error } type PaymentRepository interface { Create(ctx context.Context, payment *entities.Payment) error GetByID(ctx context.Context, id uuid.UUID) (*entities.Payment, error) GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.Payment, error) Update(ctx context.Context, payment *entities.Payment) error Delete(ctx context.Context, id uuid.UUID) error RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error) CreatePaymentWithInventoryMovement(ctx context.Context, req *models.CreatePaymentRequest, order *entities.Order, totalPaid float64) (*entities.Payment, error) 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) 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 } type PaymentMethodRepository interface { GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) } type CustomerRepository interface { GetByIDAndOrganization(ctx context.Context, id, organizationID uuid.UUID) (*entities.Customer, error) } type SimplePaymentMethodRepository struct{} func (r *SimplePaymentMethodRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) { // TODO: Implement proper payment method repository // For now, return a mock payment method return &entities.PaymentMethod{ ID: id, Name: "Cash", Type: entities.PaymentMethodTypeCash, }, nil } type OrderProcessorImpl struct { orderRepo OrderRepository orderItemRepo OrderItemRepository paymentRepo PaymentRepository paymentOrderItemRepo PaymentOrderItemRepository productRepo ProductRepository paymentMethodRepo PaymentMethodRepository inventoryRepo repository.InventoryRepository inventoryMovementRepo repository.InventoryMovementRepository productVariantRepo repository.ProductVariantRepository outletRepo OutletRepository customerRepo CustomerRepository splitBillProcessor SplitBillProcessor } func NewOrderProcessorImpl( orderRepo OrderRepository, orderItemRepo OrderItemRepository, paymentRepo PaymentRepository, paymentOrderItemRepo PaymentOrderItemRepository, productRepo ProductRepository, paymentMethodRepo PaymentMethodRepository, inventoryRepo repository.InventoryRepository, inventoryMovementRepo repository.InventoryMovementRepository, productVariantRepo repository.ProductVariantRepository, outletRepo OutletRepository, customerRepo CustomerRepository, ) *OrderProcessorImpl { return &OrderProcessorImpl{ orderRepo: orderRepo, orderItemRepo: orderItemRepo, paymentRepo: paymentRepo, paymentOrderItemRepo: paymentOrderItemRepo, productRepo: productRepo, paymentMethodRepo: paymentMethodRepo, inventoryRepo: inventoryRepo, inventoryMovementRepo: inventoryMovementRepo, productVariantRepo: productVariantRepo, outletRepo: outletRepo, customerRepo: customerRepo, splitBillProcessor: NewSplitBillProcessorImpl(orderRepo, orderItemRepo, paymentRepo, paymentOrderItemRepo, outletRepo), } } func (p *OrderProcessorImpl) CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) { orderNumber, err := p.orderRepo.GetNextOrderNumber(ctx, organizationID, req.OutletID) if err != nil { return nil, fmt.Errorf("failed to generate order number: %w", err) } outlet, err := p.outletRepo.GetByID(ctx, req.OutletID) if err != nil { return nil, fmt.Errorf("outlet not found: %w", err) } var subtotal, totalCost float64 var orderItems []*entities.OrderItem for _, itemReq := range req.OrderItems { product, err := p.productRepo.GetByID(ctx, itemReq.ProductID) if err != nil { return nil, fmt.Errorf("product not found: %w", err) } unitPrice := product.Price unitCost := product.Cost if itemReq.ProductVariantID != nil { variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID) if err != nil { return nil, fmt.Errorf("product variant not found: %w", err) } if variant.ProductID != itemReq.ProductID { return nil, fmt.Errorf("product variant does not belong to the specified product") } unitPrice += variant.PriceModifier if variant.Cost > 0 { unitCost = variant.Cost } } itemTotalPrice := float64(itemReq.Quantity) * unitPrice itemTotalCost := float64(itemReq.Quantity) * unitCost subtotal += itemTotalPrice totalCost += itemTotalCost orderItem := &entities.OrderItem{ ProductID: itemReq.ProductID, ProductVariantID: itemReq.ProductVariantID, Quantity: itemReq.Quantity, UnitPrice: unitPrice, TotalPrice: itemTotalPrice, UnitCost: unitCost, TotalCost: itemTotalCost, Modifiers: entities.Modifiers(itemReq.Modifiers), Notes: itemReq.Notes, Metadata: entities.Metadata(itemReq.Metadata), Status: entities.OrderItemStatusPending, } orderItems = append(orderItems, orderItem) } taxAmount := subtotal * outlet.TaxRate totalAmount := subtotal + taxAmount metadata := entities.Metadata(req.Metadata) if req.CustomerName != nil { if metadata == nil { metadata = make(entities.Metadata) } 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, 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 { return nil, fmt.Errorf("failed to create order: %w", err) } for _, orderItem := range orderItems { orderItem.OrderID = order.ID if err := p.orderItemRepo.Create(ctx, orderItem); err != nil { return nil, fmt.Errorf("failed to create order item: %w", err) } } orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, order.ID) if err != nil { return nil, fmt.Errorf("failed to retrieve created order: %w", err) } response := mappers.OrderEntityToResponse(orderWithRelations) return response, nil } func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) { order, err := p.orderRepo.GetByID(ctx, orderID) if err != nil { return nil, fmt.Errorf("order not found: %w", err) } if order.IsVoid { return nil, fmt.Errorf("cannot modify voided order") } if order.PaymentStatus == entities.PaymentStatusCompleted { return nil, fmt.Errorf("cannot modify fully paid order") } // Get outlet information for tax rate outlet, err := p.outletRepo.GetByID(ctx, order.OutletID) if err != nil { return nil, fmt.Errorf("outlet not found: %w", err) } var newSubtotal, newTotalCost float64 var addedOrderItems []*entities.OrderItem for _, itemReq := range req.OrderItems { product, err := p.productRepo.GetByID(ctx, itemReq.ProductID) if err != nil { return nil, fmt.Errorf("product not found: %w", err) } // Use product price from database unitPrice := product.Price unitCost := product.Cost // Handle product variant if specified if itemReq.ProductVariantID != nil { variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID) if err != nil { return nil, fmt.Errorf("product variant not found: %w", err) } // Verify variant belongs to the product if variant.ProductID != itemReq.ProductID { return nil, fmt.Errorf("product variant does not belong to the specified product") } // Apply price modifier unitPrice += variant.PriceModifier // Use variant cost if available, otherwise use product cost if variant.Cost > 0 { unitCost = variant.Cost } } itemTotalPrice := float64(itemReq.Quantity) * unitPrice itemTotalCost := float64(itemReq.Quantity) * unitCost newSubtotal += itemTotalPrice newTotalCost += itemTotalCost orderItem := &entities.OrderItem{ OrderID: orderID, ProductID: itemReq.ProductID, ProductVariantID: itemReq.ProductVariantID, Quantity: itemReq.Quantity, UnitPrice: unitPrice, // Use price from database TotalPrice: itemTotalPrice, UnitCost: unitCost, TotalCost: itemTotalCost, Modifiers: entities.Modifiers(itemReq.Modifiers), Notes: itemReq.Notes, Metadata: entities.Metadata(itemReq.Metadata), Status: entities.OrderItemStatusPending, } addedOrderItems = append(addedOrderItems, orderItem) } order.Subtotal += newSubtotal order.TotalCost += newTotalCost // Recalculate tax amount using outlet's tax rate 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) } for k, v := range req.Metadata { order.Metadata[k] = v } } if err := p.orderRepo.Update(ctx, order); err != nil { return nil, fmt.Errorf("failed to update order: %w", err) } var addedItemResponses []models.OrderItemResponse for _, orderItem := range addedOrderItems { if err := p.orderItemRepo.Create(ctx, orderItem); err != nil { return nil, fmt.Errorf("failed to create order item: %w", err) } itemResponse := models.OrderItemResponse{ ID: orderItem.ID, OrderID: orderItem.OrderID, ProductID: orderItem.ProductID, ProductVariantID: orderItem.ProductVariantID, Quantity: orderItem.Quantity, UnitPrice: orderItem.UnitPrice, TotalPrice: orderItem.TotalPrice, UnitCost: orderItem.UnitCost, TotalCost: orderItem.TotalCost, RefundAmount: orderItem.RefundAmount, RefundQuantity: orderItem.RefundQuantity, IsPartiallyRefunded: orderItem.IsPartiallyRefunded, IsFullyRefunded: orderItem.IsFullyRefunded, RefundReason: orderItem.RefundReason, RefundedAt: orderItem.RefundedAt, RefundedBy: orderItem.RefundedBy, Modifiers: []map[string]interface{}(orderItem.Modifiers), Notes: orderItem.Notes, Metadata: map[string]interface{}(orderItem.Metadata), Status: constants.OrderItemStatus(orderItem.Status), CreatedAt: orderItem.CreatedAt, UpdatedAt: orderItem.UpdatedAt, } addedItemResponses = append(addedItemResponses, itemResponse) } orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID) if err != nil { return nil, fmt.Errorf("failed to retrieve updated order: %w", err) } updatedOrderResponse := mappers.OrderEntityToResponse(orderWithRelations) return &models.AddToOrderResponse{ OrderID: orderID, OrderNumber: order.OrderNumber, AddedItems: addedItemResponses, UpdatedOrder: *updatedOrderResponse, }, nil } func (p *OrderProcessorImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) { // Get existing order order, err := p.orderRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("order not found: %w", err) } // Check if order can be modified if order.IsVoid { return nil, fmt.Errorf("cannot modify voided order") } // Apply updates if req.TableNumber != nil { order.TableNumber = req.TableNumber } if req.Status != nil { order.Status = entities.OrderStatus(*req.Status) } if req.DiscountAmount != nil { 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 { order.Metadata = make(entities.Metadata) } for k, v := range req.Metadata { order.Metadata[k] = v } } // Update order if err := p.orderRepo.Update(ctx, order); err != nil { return nil, fmt.Errorf("failed to update order: %w", err) } // Get updated order with relations orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, id) if err != nil { return nil, fmt.Errorf("failed to retrieve updated order: %w", err) } response := mappers.OrderEntityToResponse(orderWithRelations) return response, nil } func (p *OrderProcessorImpl) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) { order, err := p.orderRepo.GetWithRelations(ctx, id) if err != nil { return nil, fmt.Errorf("order not found: %w", err) } response := mappers.OrderEntityToResponse(order) return response, nil } func (p *OrderProcessorImpl) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) { filters := make(map[string]interface{}) if req.OrganizationID != nil { filters["organization_id"] = *req.OrganizationID } if req.OutletID != nil { filters["outlet_id"] = *req.OutletID } if req.UserID != nil { filters["user_id"] = *req.UserID } if req.CustomerID != nil { filters["customer_id"] = *req.CustomerID } if req.OrderType != nil { filters["order_type"] = string(*req.OrderType) } if req.Status != nil { filters["status"] = string(*req.Status) } if req.PaymentStatus != nil { filters["payment_status"] = string(*req.PaymentStatus) } if req.IsVoid != nil { filters["is_void"] = *req.IsVoid } if req.IsRefund != nil { filters["is_refund"] = *req.IsRefund } if req.DateFrom != nil { filters["date_from"] = *req.DateFrom } if req.DateTo != nil { filters["date_to"] = *req.DateTo } if req.Search != "" { filters["search"] = req.Search } offset := (req.Page - 1) * req.Limit orders, total, err := p.orderRepo.List(ctx, filters, req.Limit, offset) if err != nil { return nil, fmt.Errorf("failed to list orders: %w", err) } 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...) } } } totalPages := int(total) / req.Limit if int(total)%req.Limit > 0 { totalPages++ } return &models.ListOrdersResponse{ Orders: orderResponses, Payments: allPayments, TotalCount: int(total), Page: req.Page, Limit: req.Limit, TotalPages: totalPages, }, nil } func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error { if req.OrderID != req.OrderID { return fmt.Errorf("order ID mismatch: path parameter does not match request body") } order, err := p.orderRepo.GetByID(ctx, req.OrderID) if err != nil { return fmt.Errorf("order not found: %w", err) } if order.IsVoid { return fmt.Errorf("order is already voided") } if order.PaymentStatus == entities.PaymentStatusCompleted { return fmt.Errorf("cannot void fully paid order") } if req.Type == "ALL" { if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil { return fmt.Errorf("failed to void order: %w", err) } } else if req.Type == "ITEM" { if len(req.Items) == 0 { return fmt.Errorf("items list is required when voiding specific items") } var totalVoidedAmount float64 var totalVoidedCost float64 for _, itemVoid := range req.Items { orderItemID := itemVoid.OrderItemID orderItem, err := p.orderItemRepo.GetByID(ctx, orderItemID) if err != nil { return fmt.Errorf("order item not found: %w", err) } if orderItem.OrderID != req.OrderID { return fmt.Errorf("order item does not belong to this order") } if itemVoid.Quantity > orderItem.Quantity { return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID) } voidedAmount := float64(itemVoid.Quantity) * orderItem.UnitPrice voidedCost := float64(itemVoid.Quantity) * orderItem.UnitCost totalVoidedAmount += voidedAmount totalVoidedCost += voidedCost if err := p.orderItemRepo.VoidOrderItem(ctx, orderItemID, itemVoid.Quantity, req.Reason, voidedBy); err != nil { return fmt.Errorf("failed to void order item %d: %w", itemVoid.OrderItemID, err) } } outlet, err := p.outletRepo.GetByID(ctx, order.OutletID) if err != nil { return fmt.Errorf("outlet not found: %w", err) } order.Subtotal -= totalVoidedAmount order.TotalCost -= totalVoidedCost order.TaxAmount = order.Subtotal * outlet.TaxRate // Recalculate tax using outlet's tax rate order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount if err := p.orderRepo.Update(ctx, order); err != nil { return fmt.Errorf("failed to update order totals: %w", err) } remainingItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID) if err != nil { return fmt.Errorf("failed to get remaining order items: %w", err) } hasActiveItems := false for _, item := range remainingItems { if item.Status != entities.OrderItemStatusCancelled { hasActiveItems = true break } } if !hasActiveItems { if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil { return fmt.Errorf("failed to void order after all items voided: %w", err) } } } else { return fmt.Errorf("invalid void type: must be 'ALL' or 'ITEM'") } return nil } func (p *OrderProcessorImpl) RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error { order, err := p.orderRepo.GetWithRelations(ctx, id) if err != nil { return fmt.Errorf("order not found: %w", err) } if order.IsRefund { return fmt.Errorf("order is already refunded") } if order.PaymentStatus != entities.PaymentStatusCompleted { return fmt.Errorf("order is not paid, cannot refund") } reason := "No reason provided" if req.Reason != nil { reason = *req.Reason } // Process refund based on request type if req.RefundAmount != nil { // Full or partial refund by amount if *req.RefundAmount > order.TotalAmount { return fmt.Errorf("refund amount cannot exceed order total") } // Update order refund amount order.RefundAmount = *req.RefundAmount if err := p.orderRepo.Update(ctx, order); err != nil { return fmt.Errorf("failed to update order refund amount: %w", err) } // Mark order as refunded if err := p.orderRepo.RefundOrder(ctx, id, reason, refundedBy); err != nil { return fmt.Errorf("failed to mark order as refunded: %w", err) } } else if len(req.OrderItems) > 0 { // Refund by specific items totalRefundAmount := float64(0) for _, itemRefund := range req.OrderItems { // Get order item orderItem, err := p.orderItemRepo.GetByID(ctx, itemRefund.OrderItemID) if err != nil { return fmt.Errorf("order item not found: %w", err) } if orderItem.OrderID != id { return fmt.Errorf("order item does not belong to this order") } // Calculate refund amount for this item refundQuantity := itemRefund.RefundQuantity if refundQuantity == 0 { refundQuantity = orderItem.Quantity } if refundQuantity > orderItem.Quantity { return fmt.Errorf("refund quantity cannot exceed original quantity") } refundAmount := float64(refundQuantity) * orderItem.UnitPrice if itemRefund.RefundAmount != nil { refundAmount = *itemRefund.RefundAmount } // Process item refund itemReason := reason if itemRefund.Reason != nil { itemReason = *itemRefund.Reason } if err := p.orderItemRepo.RefundOrderItem(ctx, itemRefund.OrderItemID, refundQuantity, refundAmount, itemReason, refundedBy); err != nil { return fmt.Errorf("failed to refund order item: %w", err) } totalRefundAmount += refundAmount } // Update order refund amount order.RefundAmount = totalRefundAmount if err := p.orderRepo.Update(ctx, order); err != nil { return fmt.Errorf("failed to update order refund amount: %w", err) } // Mark order as refunded if err := p.orderRepo.RefundOrder(ctx, id, reason, refundedBy); err != nil { return fmt.Errorf("failed to mark order as refunded: %w", err) } } return nil } func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) { order, err := p.orderRepo.GetByID(ctx, req.OrderID) if err != nil { return nil, fmt.Errorf("order not found: %w", err) } if order.IsVoid { return nil, fmt.Errorf("cannot process payment for voided order") } if order.PaymentStatus == entities.PaymentStatusCompleted { return nil, fmt.Errorf("order is already fully paid") } _, err = p.paymentMethodRepo.GetByID(ctx, req.PaymentMethodID) if err != nil { return nil, fmt.Errorf("payment method not found: %w", err) } totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, req.OrderID) if err != nil { return nil, fmt.Errorf("failed to get total paid: %w", err) } remainingAmount := order.TotalAmount - totalPaid if req.Amount > remainingAmount { return nil, fmt.Errorf("payment amount exceeds remaining balance") } payment, err := p.paymentRepo.CreatePaymentWithInventoryMovement(ctx, req, order, totalPaid) if err != nil { 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) } response := mappers.PaymentEntityToResponse(paymentWithRelations) return response, nil } 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 { return fmt.Errorf("payment not found: %w", err) } if payment.Status != entities.PaymentTransactionStatusCompleted { return fmt.Errorf("payment is not completed, cannot refund") } if refundAmount > payment.Amount { return fmt.Errorf("refund amount cannot exceed payment amount") } return p.paymentRepo.RefundPaymentWithInventoryMovement(ctx, paymentID, refundAmount, reason, refundedBy, payment) } func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) { order, err := p.orderRepo.GetByID(ctx, orderID) if err != nil { return nil, fmt.Errorf("order not found: %w", err) } if order.OrganizationID != organizationID { return nil, fmt.Errorf("order does not belong to the organization") } if order.Status != entities.OrderStatusPending { return nil, fmt.Errorf("customer can only be set for pending orders") } // Verify customer exists and belongs to the organization customer, err := p.customerRepo.GetByIDAndOrganization(ctx, req.CustomerID, organizationID) if err != nil { return nil, fmt.Errorf("customer not found or does not belong to the organization: %w", err) } // Update order with customer ID order.CustomerID = &req.CustomerID if err := p.orderRepo.Update(ctx, order); err != nil { return nil, fmt.Errorf("failed to update order with customer: %w", err) } response := &models.SetOrderCustomerResponse{ OrderID: orderID, CustomerID: req.CustomerID, Message: fmt.Sprintf("Customer '%s' successfully set for order", customer.Name), } 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.splitBillProcessor.SplitByAmount(ctx, req, order, payment, customer) } else if req.IsItem() { 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 }