1605 lines
58 KiB
Go
1605 lines
58 KiB
Go
package processor
|
|
|
|
import (
|
|
"apskel-pos-be/internal/constants"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"apskel-pos-be/internal/entities"
|
|
"apskel-pos-be/internal/mappers"
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/repository"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
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 InventoryMovementService interface {
|
|
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
|
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
|
}
|
|
|
|
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
|
|
txManager *repository.TxManager
|
|
productRecipeRepo *repository.ProductRecipeRepository
|
|
ingredientRepo IngredientRepository
|
|
inventoryMovementService InventoryMovementService
|
|
}
|
|
|
|
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,
|
|
txManager *repository.TxManager,
|
|
productRecipeRepo *repository.ProductRecipeRepository,
|
|
ingredientRepo IngredientRepository,
|
|
inventoryMovementService InventoryMovementService,
|
|
) *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),
|
|
txManager: txManager,
|
|
productRecipeRepo: productRecipeRepo,
|
|
ingredientRepo: ingredientRepo,
|
|
inventoryMovementService: inventoryMovementService,
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
payment, err := p.CreatePaymentWithInventoryMovement(ctx, req, order, totalPaid)
|
|
if err != nil {
|
|
return nil, 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.RefundPaymentWithInventoryMovement(ctx, paymentID, refundAmount, reason, refundedBy, payment)
|
|
}
|
|
|
|
func (p *OrderProcessorImpl) CreatePaymentWithInventoryMovement(ctx context.Context, req *models.CreatePaymentRequest, order *entities.Order, totalPaid float64) (*entities.Payment, error) {
|
|
var payment *entities.Payment
|
|
|
|
err := p.txManager.WithTransaction(ctx, func(ctx context.Context) error {
|
|
var err error
|
|
payment, err = p.createPayment(ctx, req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create payment: %w", err)
|
|
}
|
|
|
|
if err := p.updateOrderStatus(ctx, req.OrderID); err != nil {
|
|
return fmt.Errorf("failed to update order status: %w", err)
|
|
}
|
|
|
|
if err := p.processInventoryAdjustments(ctx, req.OrderID, order, payment); err != nil {
|
|
return fmt.Errorf("failed to process inventory adjustments: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return payment, nil
|
|
}
|
|
|
|
func (p *OrderProcessorImpl) createPayment(ctx context.Context, req *models.CreatePaymentRequest) (*entities.Payment, error) {
|
|
payment := &entities.Payment{
|
|
OrderID: req.OrderID,
|
|
PaymentMethodID: req.PaymentMethodID,
|
|
Amount: req.Amount,
|
|
Status: entities.PaymentTransactionStatusCompleted,
|
|
TransactionID: req.TransactionID,
|
|
SplitNumber: req.SplitNumber,
|
|
SplitTotal: req.SplitTotal,
|
|
SplitDescription: req.SplitDescription,
|
|
Metadata: entities.Metadata(req.Metadata),
|
|
}
|
|
|
|
if err := p.paymentRepo.Create(ctx, payment); err != nil {
|
|
return nil, fmt.Errorf("failed to create payment: %w", err)
|
|
}
|
|
|
|
return payment, nil
|
|
}
|
|
|
|
func (p *OrderProcessorImpl) updateOrderStatus(ctx context.Context, orderID uuid.UUID) error {
|
|
orderUpdate := &entities.Order{
|
|
ID: orderID,
|
|
Status: entities.OrderStatusCompleted,
|
|
PaymentStatus: entities.PaymentStatusCompleted,
|
|
}
|
|
|
|
if err := p.orderRepo.Update(ctx, orderUpdate); err != nil {
|
|
return fmt.Errorf("failed to update order status: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *OrderProcessorImpl) processInventoryAdjustments(ctx context.Context, orderID uuid.UUID, order *entities.Order, payment *entities.Payment) error {
|
|
orderItems, err := p.orderItemRepo.GetByOrderID(ctx, orderID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
|
|
}
|
|
|
|
var inventoryMovements []*entities.InventoryMovement
|
|
var inventoryUpdates []*entities.Inventory
|
|
var ingredientUpdates []*entities.Ingredient
|
|
var ingredientMovements []*entities.InventoryMovement
|
|
|
|
for _, item := range orderItems {
|
|
updatedInventory, err := p.prepareProductInventoryUpdate(ctx, item, order.OutletID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prepare product inventory update for product %s: %w", item.ProductID, err)
|
|
}
|
|
inventoryUpdates = append(inventoryUpdates, updatedInventory)
|
|
|
|
productMovement := p.prepareProductInventoryMovement(item, order, payment, updatedInventory)
|
|
inventoryMovements = append(inventoryMovements, productMovement)
|
|
|
|
ingredientData, err := p.prepareIngredientRecipeData(ctx, item, order, payment)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prepare ingredient recipe data for product %s: %w", item.ProductID, err)
|
|
}
|
|
|
|
ingredientUpdates = append(ingredientUpdates, ingredientData.ingredientUpdates...)
|
|
ingredientMovements = append(ingredientMovements, ingredientData.movements...)
|
|
}
|
|
|
|
if len(inventoryUpdates) > 0 {
|
|
if err := p.bulkUpdateInventory(ctx, inventoryUpdates); err != nil {
|
|
return fmt.Errorf("failed to bulk update product inventory: %w", err)
|
|
}
|
|
}
|
|
|
|
allMovements := append(inventoryMovements, ingredientMovements...)
|
|
if len(allMovements) > 0 {
|
|
if err := p.bulkCreateInventoryMovements(ctx, allMovements); err != nil {
|
|
return fmt.Errorf("failed to bulk create inventory movements: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// adjustIngredientInventoryWithTransaction adjusts ingredient inventory within a transaction
|
|
func (p *OrderProcessorImpl) adjustIngredientInventoryWithTransaction(ctx context.Context, ingredientID, outletID uuid.UUID, delta float64) (*entities.Inventory, error) {
|
|
var inventory entities.Inventory
|
|
|
|
// Try to get existing ingredient inventory
|
|
existingInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, ingredientID, outletID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
// Create new ingredient inventory record if it doesn't exist
|
|
inventory = entities.Inventory{
|
|
ProductID: ingredientID,
|
|
OutletID: outletID,
|
|
Quantity: 0,
|
|
ReorderLevel: 0,
|
|
}
|
|
if err := p.inventoryRepo.Create(ctx, &inventory); err != nil {
|
|
return nil, fmt.Errorf("failed to create ingredient inventory record: %w", err)
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Use existing ingredient inventory
|
|
inventory = *existingInventory
|
|
}
|
|
|
|
// Update quantity (note: ingredients use float64 quantities, but inventory uses int)
|
|
// Convert delta to int for inventory system
|
|
deltaInt := int(delta)
|
|
inventory.Quantity += deltaInt
|
|
if inventory.Quantity < 0 {
|
|
inventory.Quantity = 0
|
|
}
|
|
|
|
if err := p.inventoryRepo.Update(ctx, &inventory); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &inventory, nil
|
|
}
|
|
|
|
// createIngredientInventoryMovement creates an inventory movement record for ingredient usage
|
|
func (p *OrderProcessorImpl) createIngredientInventoryMovement(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, updatedInventory *entities.Inventory, totalIngredientQuantity float64) error {
|
|
ingredient, err := p.getIngredientDetails(ctx, recipe.IngredientID, order.OrganizationID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get ingredient details: %w", err)
|
|
}
|
|
|
|
movement := &entities.InventoryMovement{
|
|
OrganizationID: order.OrganizationID,
|
|
OutletID: order.OutletID,
|
|
ItemID: recipe.IngredientID,
|
|
ItemType: "INGREDIENT",
|
|
MovementType: entities.InventoryMovementTypeIngredient,
|
|
Quantity: -totalIngredientQuantity, // Negative because we're consuming ingredients
|
|
PreviousQuantity: float64(updatedInventory.Quantity + int(totalIngredientQuantity)), // Add back the quantity that was subtracted
|
|
NewQuantity: float64(updatedInventory.Quantity),
|
|
UnitCost: ingredient.Cost, // Use ingredient cost
|
|
TotalCost: totalIngredientQuantity * ingredient.Cost,
|
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
|
t := entities.InventoryMovementReferenceTypePayment
|
|
return &t
|
|
}(),
|
|
ReferenceID: &payment.ID,
|
|
OrderID: &order.ID,
|
|
PaymentID: &payment.ID,
|
|
UserID: order.UserID,
|
|
Reason: stringPtr("Ingredient consumption from order payment"),
|
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Product: %s, Ingredient: %s", order.OrderNumber, payment.ID, item.Product.Name, ingredient.Name)),
|
|
Metadata: entities.Metadata{"order_item_id": item.ID, "product_id": item.ProductID, "ingredient_id": recipe.IngredientID, "recipe_quantity": recipe.Quantity, "order_quantity": item.Quantity},
|
|
}
|
|
|
|
if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
|
|
return fmt.Errorf("failed to create ingredient inventory movement: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getIngredientDetails retrieves ingredient details for cost calculation
|
|
func (p *OrderProcessorImpl) getIngredientDetails(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) (*entities.Ingredient, error) {
|
|
ingredient, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get ingredient details: %w", err)
|
|
}
|
|
return ingredient, nil
|
|
}
|
|
|
|
// createInventoryMovement creates an inventory movement record for audit trail
|
|
func (p *OrderProcessorImpl) createInventoryMovement(ctx context.Context, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, updatedInventory *entities.Inventory) error {
|
|
movement := &entities.InventoryMovement{
|
|
OrganizationID: order.OrganizationID,
|
|
OutletID: order.OutletID,
|
|
ItemID: item.ProductID,
|
|
ItemType: "PRODUCT",
|
|
MovementType: entities.InventoryMovementTypeSale,
|
|
Quantity: float64(-item.Quantity),
|
|
PreviousQuantity: float64(updatedInventory.Quantity + item.Quantity), // Add back the quantity that was subtracted
|
|
NewQuantity: float64(updatedInventory.Quantity),
|
|
UnitCost: item.UnitCost,
|
|
TotalCost: float64(item.Quantity) * item.UnitCost,
|
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
|
t := entities.InventoryMovementReferenceTypePayment
|
|
return &t
|
|
}(),
|
|
ReferenceID: &payment.ID,
|
|
OrderID: &order.ID,
|
|
PaymentID: &payment.ID,
|
|
UserID: order.UserID,
|
|
Reason: stringPtr("Sale from order payment"),
|
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s", order.OrderNumber, payment.ID)),
|
|
Metadata: entities.Metadata{"order_item_id": item.ID},
|
|
}
|
|
|
|
if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
|
|
return fmt.Errorf("failed to create inventory movement: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *OrderProcessorImpl) RefundPaymentWithInventoryMovement(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID, payment *entities.Payment) error {
|
|
return p.txManager.WithTransaction(ctx, func(ctx context.Context) error {
|
|
if err := p.processRefund(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
|
|
return fmt.Errorf("failed to process refund: %w", err)
|
|
}
|
|
|
|
if err := p.updateOrderRefundAmount(ctx, payment.OrderID, refundAmount); err != nil {
|
|
return fmt.Errorf("failed to update order refund amount: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (p *OrderProcessorImpl) processRefund(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error {
|
|
if err := p.paymentRepo.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
|
|
return fmt.Errorf("failed to refund payment: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *OrderProcessorImpl) updateOrderRefundAmount(ctx context.Context, orderID uuid.UUID, refundAmount float64) error {
|
|
order, err := p.orderRepo.GetByID(ctx, orderID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get order: %w", err)
|
|
}
|
|
|
|
order.RefundAmount += refundAmount
|
|
if err := p.orderRepo.Update(ctx, order); err != nil {
|
|
return fmt.Errorf("failed to update order refund amount: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Helper method to adjust inventory within a transaction
|
|
func (p *OrderProcessorImpl) adjustInventoryWithTransaction(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error) {
|
|
var inventory entities.Inventory
|
|
|
|
// Try to get existing inventory
|
|
existingInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
// Create new inventory record if it doesn't exist
|
|
inventory = entities.Inventory{
|
|
ProductID: productID,
|
|
OutletID: outletID,
|
|
Quantity: 0,
|
|
ReorderLevel: 0,
|
|
}
|
|
if err := p.inventoryRepo.Create(ctx, &inventory); err != nil {
|
|
return nil, fmt.Errorf("failed to create inventory record: %w", err)
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Use existing inventory
|
|
inventory = *existingInventory
|
|
}
|
|
|
|
// Update quantity
|
|
inventory.UpdateQuantity(delta)
|
|
if err := p.inventoryRepo.Update(ctx, &inventory); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &inventory, nil
|
|
}
|
|
|
|
func (p *OrderProcessorImpl) createIngredientProductMovement(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, totalIngredientQuantity float64) error {
|
|
ingredient, err := p.getIngredientDetails(ctx, recipe.IngredientID, order.OrganizationID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get ingredient details: %w", err)
|
|
}
|
|
|
|
err = p.inventoryMovementService.CreateProductMovement(
|
|
ctx,
|
|
recipe.IngredientID, // ingredientID as productID
|
|
order.OrganizationID,
|
|
order.OutletID,
|
|
order.UserID,
|
|
entities.InventoryMovementTypeSale, // Movement Type "Sales"
|
|
-totalIngredientQuantity, // Negative quantity for consumption
|
|
ingredient.Cost, // Unit cost from ingredient
|
|
"Ingredient consumption from order payment",
|
|
func() *entities.InventoryMovementReferenceType {
|
|
t := entities.InventoryMovementReferenceTypePayment
|
|
return &t
|
|
}(),
|
|
&payment.ID,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create ingredient product movement: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createRefundIngredientProductMovement creates a product movement record for ingredient with Movement Type "Refund" and ItemType "INGREDIENT"
|
|
func (p *OrderProcessorImpl) createRefundIngredientProductMovement(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, totalIngredientQuantity float64, refundRatio float64, reason string, refundedBy uuid.UUID, refundAmount float64) error {
|
|
// Get ingredient details for cost calculation
|
|
ingredient, err := p.getIngredientDetails(ctx, recipe.IngredientID, order.OrganizationID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get ingredient details: %w", err)
|
|
}
|
|
|
|
// Create product movement using the inventory movement service
|
|
err = p.inventoryMovementService.CreateProductMovement(
|
|
ctx,
|
|
recipe.IngredientID, // ingredientID as productID
|
|
order.OrganizationID,
|
|
order.OutletID,
|
|
order.UserID,
|
|
entities.InventoryMovementTypeRefund, // Movement Type "Refund"
|
|
totalIngredientQuantity, // Positive quantity for restoration
|
|
ingredient.Cost, // Unit cost from ingredient
|
|
fmt.Sprintf("Ingredient restoration from order refund: %s", reason),
|
|
func() *entities.InventoryMovementReferenceType {
|
|
t := entities.InventoryMovementReferenceTypeRefund
|
|
return &t
|
|
}(),
|
|
&payment.ID,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create refund ingredient product movement: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// prepareProductInventoryUpdate prepares inventory update data without making database calls
|
|
func (p *OrderProcessorImpl) prepareProductInventoryUpdate(ctx context.Context, item *entities.OrderItem, outletID uuid.UUID) (*entities.Inventory, error) {
|
|
// Get current inventory
|
|
currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, item.ProductID, outletID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
// Create new inventory record if it doesn't exist
|
|
currentInventory = &entities.Inventory{
|
|
ProductID: item.ProductID,
|
|
OutletID: outletID,
|
|
Quantity: 0,
|
|
ReorderLevel: 0,
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Calculate new quantity
|
|
newQuantity := currentInventory.Quantity - item.Quantity
|
|
if newQuantity < 0 {
|
|
newQuantity = 0
|
|
}
|
|
|
|
// Update the inventory object (don't save to DB yet)
|
|
currentInventory.Quantity = newQuantity
|
|
return currentInventory, nil
|
|
}
|
|
|
|
// prepareProductInventoryMovement prepares inventory movement data without making database calls
|
|
func (p *OrderProcessorImpl) prepareProductInventoryMovement(item *entities.OrderItem, order *entities.Order, payment *entities.Payment, updatedInventory *entities.Inventory) *entities.InventoryMovement {
|
|
previousQuantity := updatedInventory.Quantity + item.Quantity
|
|
|
|
return &entities.InventoryMovement{
|
|
OrganizationID: order.OrganizationID,
|
|
OutletID: order.OutletID,
|
|
ItemID: item.ProductID,
|
|
ItemType: "PRODUCT",
|
|
MovementType: entities.InventoryMovementTypeSale,
|
|
Quantity: float64(-item.Quantity),
|
|
PreviousQuantity: float64(previousQuantity),
|
|
NewQuantity: float64(updatedInventory.Quantity),
|
|
UnitCost: item.UnitCost,
|
|
TotalCost: float64(item.Quantity) * item.UnitCost,
|
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
|
t := entities.InventoryMovementReferenceTypePayment
|
|
return &t
|
|
}(),
|
|
ReferenceID: &payment.ID,
|
|
OrderID: &order.ID,
|
|
PaymentID: &payment.ID,
|
|
UserID: order.UserID,
|
|
Reason: stringPtr("Sale from order payment"),
|
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s", order.OrderNumber, payment.ID)),
|
|
Metadata: entities.Metadata{"order_item_id": item.ID},
|
|
}
|
|
}
|
|
|
|
// ingredientRecipeData holds the collected data for ingredient recipes
|
|
type ingredientRecipeData struct {
|
|
ingredientUpdates []*entities.Ingredient
|
|
movements []*entities.InventoryMovement
|
|
}
|
|
|
|
// prepareIngredientRecipeData prepares ingredient recipe data without making database calls
|
|
func (p *OrderProcessorImpl) prepareIngredientRecipeData(ctx context.Context, item *entities.OrderItem, order *entities.Order, payment *entities.Payment) (*ingredientRecipeData, error) {
|
|
// Check if the product has ingredients
|
|
product, err := p.productRepo.GetByID(ctx, item.ProductID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get product: %w", err)
|
|
}
|
|
|
|
if !product.HasIngredients {
|
|
return &ingredientRecipeData{}, nil // Product doesn't have ingredients
|
|
}
|
|
|
|
// Get product recipes based on variant (if any)
|
|
var recipes []*entities.ProductRecipe
|
|
if item.ProductVariantID != nil {
|
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, item.ProductVariantID, order.OrganizationID)
|
|
} else {
|
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, nil, order.OrganizationID)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get product recipes: %w", err)
|
|
}
|
|
|
|
if len(recipes) == 0 {
|
|
return &ingredientRecipeData{}, nil // No recipes found
|
|
}
|
|
|
|
var ingredientUpdates []*entities.Ingredient
|
|
var movements []*entities.InventoryMovement
|
|
|
|
// Process each ingredient in the recipe
|
|
for _, recipe := range recipes {
|
|
ingredientData, err := p.prepareIngredientRecipeItem(ctx, recipe, item, order, payment)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to prepare ingredient recipe item: %w", err)
|
|
}
|
|
|
|
ingredientUpdates = append(ingredientUpdates, ingredientData.ingredient)
|
|
movements = append(movements, ingredientData.movement)
|
|
}
|
|
|
|
return &ingredientRecipeData{
|
|
ingredientUpdates: ingredientUpdates,
|
|
movements: movements,
|
|
}, nil
|
|
}
|
|
|
|
// ingredientRecipeItem holds data for a single ingredient recipe item
|
|
type ingredientRecipeItem struct {
|
|
ingredient *entities.Ingredient
|
|
movement *entities.InventoryMovement
|
|
}
|
|
|
|
// prepareIngredientRecipeItem prepares data for a single ingredient recipe without making database calls
|
|
func (p *OrderProcessorImpl) prepareIngredientRecipeItem(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment) (*ingredientRecipeItem, error) {
|
|
// Calculate total ingredient quantity needed
|
|
totalIngredientQuantity := recipe.Quantity * float64(item.Quantity)
|
|
|
|
// Get current ingredient details
|
|
currentIngredient, err := p.ingredientRepo.GetByID(ctx, recipe.IngredientID, order.OrganizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get ingredient: %w", err)
|
|
}
|
|
|
|
// For ingredients, we typically don't track quantity in the ingredient entity itself
|
|
// Instead, we create inventory movement records to track consumption
|
|
// The ingredient entity remains unchanged, but we track the movement
|
|
|
|
// Prepare movement record
|
|
movement := &entities.InventoryMovement{
|
|
OrganizationID: order.OrganizationID,
|
|
OutletID: order.OutletID,
|
|
ItemID: recipe.IngredientID,
|
|
ItemType: "INGREDIENT",
|
|
MovementType: entities.InventoryMovementTypeIngredient,
|
|
Quantity: -totalIngredientQuantity,
|
|
PreviousQuantity: 0, // We don't track current quantity in ingredient entity
|
|
NewQuantity: 0, // We don't track current quantity in ingredient entity
|
|
UnitCost: currentIngredient.Cost,
|
|
TotalCost: totalIngredientQuantity * currentIngredient.Cost,
|
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
|
t := entities.InventoryMovementReferenceTypePayment
|
|
return &t
|
|
}(),
|
|
ReferenceID: &payment.ID,
|
|
OrderID: &order.ID,
|
|
PaymentID: &payment.ID,
|
|
UserID: order.UserID,
|
|
Reason: stringPtr("Ingredient consumption from order payment"),
|
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Product: %s", order.OrderNumber, payment.ID, item.Product.Name)),
|
|
Metadata: entities.Metadata{"order_item_id": item.ID, "product_id": item.ProductID, "ingredient_id": recipe.IngredientID, "recipe_quantity": recipe.Quantity, "order_quantity": item.Quantity},
|
|
}
|
|
|
|
return &ingredientRecipeItem{
|
|
ingredient: currentIngredient, // Return unchanged ingredient
|
|
movement: movement,
|
|
}, nil
|
|
}
|
|
|
|
// bulkUpdateInventory performs bulk update of inventory records
|
|
func (p *OrderProcessorImpl) bulkUpdateInventory(ctx context.Context, inventories []*entities.Inventory) error {
|
|
if len(inventories) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Use the repository's bulk update method
|
|
return p.inventoryRepo.BulkUpdate(ctx, inventories)
|
|
}
|
|
|
|
// bulkCreateInventoryMovements performs bulk creation of inventory movement records
|
|
func (p *OrderProcessorImpl) bulkCreateInventoryMovements(ctx context.Context, movements []*entities.InventoryMovement) error {
|
|
if len(movements) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Use GORM's CreateInBatches for true bulk creation
|
|
// Convert to interface slice for GORM
|
|
movementInterfaces := make([]interface{}, len(movements))
|
|
for i, movement := range movements {
|
|
movementInterfaces[i] = movement
|
|
}
|
|
|
|
// Use the inventory movement repository's bulk create method
|
|
// Note: This assumes the repository has a bulk create method
|
|
// If not, we can implement it here using GORM's CreateInBatches
|
|
return p.inventoryMovementRepo.CreateInBatches(ctx, movements, 100)
|
|
}
|
|
|
|
// prepareRefundProductInventoryUpdate prepares product inventory restoration data without making database calls
|
|
func (p *OrderProcessorImpl) prepareRefundProductInventoryUpdate(ctx context.Context, item *entities.OrderItem, outletID uuid.UUID, refundedQuantity int) (*entities.Inventory, error) {
|
|
// Get current inventory
|
|
currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, item.ProductID, outletID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get product inventory: %w", err)
|
|
}
|
|
|
|
// Calculate new quantity (restore the refunded quantity)
|
|
newQuantity := currentInventory.Quantity + refundedQuantity
|
|
|
|
// Update the inventory object (don't save to DB yet)
|
|
currentInventory.Quantity = newQuantity
|
|
return currentInventory, nil
|
|
}
|
|
|
|
// prepareRefundProductInventoryMovement prepares product inventory movement data for refunds
|
|
func (p *OrderProcessorImpl) prepareRefundProductInventoryMovement(item *entities.OrderItem, order *entities.Order, payment *entities.Payment, updatedInventory *entities.Inventory, refundedQuantity int, refundRatio float64, reason string, refundedBy uuid.UUID, refundAmount float64) *entities.InventoryMovement {
|
|
previousQuantity := updatedInventory.Quantity - refundedQuantity
|
|
|
|
return &entities.InventoryMovement{
|
|
OrganizationID: order.OrganizationID,
|
|
OutletID: order.OutletID,
|
|
ItemID: item.ProductID,
|
|
ItemType: "PRODUCT",
|
|
MovementType: entities.InventoryMovementTypeRefund,
|
|
Quantity: float64(refundedQuantity),
|
|
PreviousQuantity: float64(previousQuantity),
|
|
NewQuantity: float64(updatedInventory.Quantity),
|
|
UnitCost: item.UnitCost,
|
|
TotalCost: float64(refundedQuantity) * item.UnitCost,
|
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
|
t := entities.InventoryMovementReferenceTypeRefund
|
|
return &t
|
|
}(),
|
|
ReferenceID: &payment.ID,
|
|
OrderID: &order.ID,
|
|
PaymentID: &payment.ID,
|
|
UserID: refundedBy,
|
|
Reason: stringPtr(fmt.Sprintf("Refund: %s", reason)),
|
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Refund Amount: %.2f", order.OrderNumber, payment.ID, refundAmount)),
|
|
Metadata: entities.Metadata{"order_item_id": item.ID, "refund_ratio": refundRatio},
|
|
}
|
|
}
|
|
|
|
// prepareRefundedIngredientRecipeData prepares ingredient recipe restoration data for refunds
|
|
func (p *OrderProcessorImpl) prepareRefundedIngredientRecipeData(ctx context.Context, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, refundRatio float64, reason string, refundedBy uuid.UUID, refundAmount float64) (*ingredientRecipeData, error) {
|
|
// Check if the product has ingredients
|
|
product, err := p.productRepo.GetByID(ctx, item.ProductID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get product: %w", err)
|
|
}
|
|
|
|
if !product.HasIngredients {
|
|
return &ingredientRecipeData{}, nil // Product doesn't have ingredients
|
|
}
|
|
|
|
// Get product recipes based on variant (if any)
|
|
var recipes []*entities.ProductRecipe
|
|
if item.ProductVariantID != nil {
|
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, item.ProductVariantID, order.OrganizationID)
|
|
} else {
|
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, nil, order.OrganizationID)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get product recipes: %w", err)
|
|
}
|
|
|
|
if len(recipes) == 0 {
|
|
return &ingredientRecipeData{}, nil // No recipes found
|
|
}
|
|
|
|
var ingredientUpdates []*entities.Ingredient
|
|
var movements []*entities.InventoryMovement
|
|
|
|
// Process each ingredient in the recipe
|
|
for _, recipe := range recipes {
|
|
ingredientData, err := p.prepareRefundedIngredientRecipeItem(ctx, recipe, item, order, payment, refundRatio, reason, refundedBy, refundAmount)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to prepare ingredient recipe restoration: %w", err)
|
|
}
|
|
|
|
ingredientUpdates = append(ingredientUpdates, ingredientData.ingredient)
|
|
movements = append(movements, ingredientData.movement)
|
|
}
|
|
|
|
return &ingredientRecipeData{
|
|
ingredientUpdates: ingredientUpdates,
|
|
movements: movements,
|
|
}, nil
|
|
}
|
|
|
|
// prepareRefundedIngredientRecipeItem prepares data for a single ingredient recipe restoration
|
|
func (p *OrderProcessorImpl) prepareRefundedIngredientRecipeItem(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, refundRatio float64, reason string, refundedBy uuid.UUID, refundAmount float64) (*ingredientRecipeItem, error) {
|
|
// Calculate total ingredient quantity needed based on order item quantity
|
|
totalIngredientQuantity := recipe.Quantity * float64(item.Quantity)
|
|
|
|
// Get current ingredient details
|
|
currentIngredient, err := p.ingredientRepo.GetByID(ctx, recipe.IngredientID, order.OrganizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get ingredient: %w", err)
|
|
}
|
|
|
|
// For ingredients, we typically don't track quantity in the ingredient entity itself
|
|
// Instead, we create inventory movement records to track restoration
|
|
// The ingredient entity remains unchanged, but we track the movement
|
|
|
|
// Prepare movement record
|
|
movement := &entities.InventoryMovement{
|
|
OrganizationID: order.OrganizationID,
|
|
OutletID: order.OutletID,
|
|
ItemID: recipe.IngredientID,
|
|
ItemType: "INGREDIENT",
|
|
MovementType: entities.InventoryMovementTypeRefund,
|
|
Quantity: totalIngredientQuantity,
|
|
PreviousQuantity: 0, // We don't track current quantity in ingredient entity
|
|
NewQuantity: 0, // We don't track current quantity in ingredient entity
|
|
UnitCost: currentIngredient.Cost,
|
|
TotalCost: totalIngredientQuantity * currentIngredient.Cost,
|
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
|
t := entities.InventoryMovementReferenceTypeRefund
|
|
return &t
|
|
}(),
|
|
ReferenceID: &payment.ID,
|
|
OrderID: &order.ID,
|
|
PaymentID: &payment.ID,
|
|
UserID: refundedBy,
|
|
Reason: stringPtr(fmt.Sprintf("Refund: %s", reason)),
|
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Refund Amount: %.2f", order.OrderNumber, payment.ID, refundAmount)),
|
|
Metadata: entities.Metadata{"order_item_id": item.ID, "refund_ratio": refundRatio},
|
|
}
|
|
|
|
return &ingredientRecipeItem{
|
|
ingredient: currentIngredient, // Return unchanged ingredient
|
|
movement: movement,
|
|
}, nil
|
|
}
|
|
|
|
// Helper function to create string pointer
|
|
func stringPtr(s string) *string {
|
|
return &s
|
|
}
|