package processor import ( "apskel-pos-be/internal/entities" "apskel-pos-be/internal/mappers" "apskel-pos-be/internal/models" "apskel-pos-be/internal/util" "context" "fmt" "time" "github.com/google/uuid" ) type OrderIngredientTransactionProcessor interface { CreateOrderIngredientTransaction(ctx context.Context, req *models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) (*models.OrderIngredientTransactionResponse, error) GetOrderIngredientTransactionByID(ctx context.Context, id, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *models.UpdateOrderIngredientTransactionRequest, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) DeleteOrderIngredientTransaction(ctx context.Context, id, organizationID uuid.UUID) error ListOrderIngredientTransactions(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, int64, error) GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) GetOrderIngredientTransactionSummary(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionSummary, error) BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) CalculateWasteQuantities(ctx context.Context, productID uuid.UUID, quantity float64, organizationID uuid.UUID) ([]*models.CreateOrderIngredientTransactionRequest, error) } type OrderIngredientTransactionProcessorImpl struct { orderIngredientTransactionRepo OrderIngredientTransactionRepository productIngredientRepo ProductIngredientRepository ingredientRepo IngredientRepository unitRepo UnitRepository } func NewOrderIngredientTransactionProcessorImpl( orderIngredientTransactionRepo OrderIngredientTransactionRepository, productIngredientRepo ProductIngredientRepository, ingredientRepo IngredientRepository, unitRepo UnitRepository, ) OrderIngredientTransactionProcessor { return &OrderIngredientTransactionProcessorImpl{ orderIngredientTransactionRepo: orderIngredientTransactionRepo, productIngredientRepo: productIngredientRepo, ingredientRepo: ingredientRepo, unitRepo: unitRepo, } } func (p *OrderIngredientTransactionProcessorImpl) CreateOrderIngredientTransaction(ctx context.Context, req *models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) (*models.OrderIngredientTransactionResponse, error) { // Validate that gross qty >= net qty if req.GrossQty < req.NetQty { return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity") } // Validate that waste qty = gross qty - net qty expectedWasteQty := req.GrossQty - req.NetQty if req.WasteQty != expectedWasteQty { return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity") } // Set transaction date if not provided transactionDate := time.Now() if req.TransactionDate != nil { transactionDate = *req.TransactionDate } // Create entity entity := &entities.OrderIngredientTransaction{ ID: uuid.New(), OrganizationID: organizationID, OutletID: &outletID, OrderID: req.OrderID, OrderItemID: req.OrderItemID, ProductID: req.ProductID, ProductVariantID: req.ProductVariantID, IngredientID: req.IngredientID, GrossQty: req.GrossQty, NetQty: req.NetQty, WasteQty: req.WasteQty, Unit: req.Unit, TransactionDate: transactionDate, CreatedBy: createdBy, } // Create in database if err := p.orderIngredientTransactionRepo.Create(ctx, entity); err != nil { return nil, fmt.Errorf("failed to create order ingredient transaction: %w", err) } // Get created entity with relations createdEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, entity.ID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get created order ingredient transaction: %w", err) } // Convert to response response := mappers.MapOrderIngredientTransactionEntityToResponse(createdEntity) return response, nil } func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionByID(ctx context.Context, id, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) { entity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID) if err != nil { return nil, fmt.Errorf("failed to get order ingredient transaction: %w", err) } response := mappers.MapOrderIngredientTransactionEntityToResponse(entity) return response, nil } func (p *OrderIngredientTransactionProcessorImpl) UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *models.UpdateOrderIngredientTransactionRequest, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) { // Get existing entity entity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID) if err != nil { return nil, fmt.Errorf("failed to get order ingredient transaction: %w", err) } // Update fields if req.GrossQty != nil { entity.GrossQty = *req.GrossQty } if req.NetQty != nil { entity.NetQty = *req.NetQty } if req.WasteQty != nil { entity.WasteQty = *req.WasteQty } if req.Unit != nil { entity.Unit = *req.Unit } if req.TransactionDate != nil { entity.TransactionDate = *req.TransactionDate } // Validate quantities if entity.GrossQty < entity.NetQty { return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity") } expectedWasteQty := entity.GrossQty - entity.NetQty if entity.WasteQty != expectedWasteQty { return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity") } // Update in database if err := p.orderIngredientTransactionRepo.Update(ctx, entity); err != nil { return nil, fmt.Errorf("failed to update order ingredient transaction: %w", err) } // Get updated entity with relations updatedEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID) if err != nil { return nil, fmt.Errorf("failed to get updated order ingredient transaction: %w", err) } response := mappers.MapOrderIngredientTransactionEntityToResponse(updatedEntity) return response, nil } func (p *OrderIngredientTransactionProcessorImpl) DeleteOrderIngredientTransaction(ctx context.Context, id, organizationID uuid.UUID) error { if err := p.orderIngredientTransactionRepo.Delete(ctx, id, organizationID); err != nil { return fmt.Errorf("failed to delete order ingredient transaction: %w", err) } return nil } func (p *OrderIngredientTransactionProcessorImpl) ListOrderIngredientTransactions(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, int64, error) { // Convert filters filters := make(map[string]interface{}) if req.OrderID != nil { filters["order_id"] = *req.OrderID } if req.OrderItemID != nil { filters["order_item_id"] = *req.OrderItemID } if req.ProductID != nil { filters["product_id"] = *req.ProductID } if req.ProductVariantID != nil { filters["product_variant_id"] = *req.ProductVariantID } if req.IngredientID != nil { filters["ingredient_id"] = *req.IngredientID } if req.StartDate != nil { filters["start_date"] = req.StartDate.Format(time.RFC3339) } if req.EndDate != nil { filters["end_date"] = req.EndDate.Format(time.RFC3339) } // Set default pagination page := req.Page if page <= 0 { page = 1 } limit := req.Limit if limit <= 0 { limit = 10 } entities, total, err := p.orderIngredientTransactionRepo.List(ctx, organizationID, filters, page, limit) if err != nil { return nil, 0, fmt.Errorf("failed to list order ingredient transactions: %w", err) } responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities) return responses, total, nil } func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) { entities, err := p.orderIngredientTransactionRepo.GetByOrderID(ctx, orderID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get order ingredient transactions by order: %w", err) } responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities) return responses, nil } func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) { entities, err := p.orderIngredientTransactionRepo.GetByOrderItemID(ctx, orderItemID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get order ingredient transactions by order item: %w", err) } responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities) return responses, nil } func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) { entities, err := p.orderIngredientTransactionRepo.GetByIngredientID(ctx, ingredientID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get order ingredient transactions by ingredient: %w", err) } responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities) return responses, nil } func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionSummary(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionSummary, error) { // Convert filters filters := make(map[string]interface{}) if req.OrderID != nil { filters["order_id"] = *req.OrderID } if req.OrderItemID != nil { filters["order_item_id"] = *req.OrderItemID } if req.ProductID != nil { filters["product_id"] = *req.ProductID } if req.ProductVariantID != nil { filters["product_variant_id"] = *req.ProductVariantID } if req.IngredientID != nil { filters["ingredient_id"] = *req.IngredientID } if req.StartDate != nil { filters["start_date"] = req.StartDate.Format(time.RFC3339) } if req.EndDate != nil { filters["end_date"] = req.EndDate.Format(time.RFC3339) } entities, err := p.orderIngredientTransactionRepo.GetSummary(ctx, organizationID, filters) if err != nil { return nil, fmt.Errorf("failed to get order ingredient transaction summary: %w", err) } summaries := mappers.MapOrderIngredientTransactionSummary(entities) return summaries, nil } func (p *OrderIngredientTransactionProcessorImpl) BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) { if len(transactions) == 0 { return []*models.OrderIngredientTransactionResponse{}, nil } // Convert to entities transactionEntities := make([]*entities.OrderIngredientTransaction, len(transactions)) for i, req := range transactions { // Validate quantities if req.GrossQty < req.NetQty { return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity for transaction %d", i) } expectedWasteQty := req.GrossQty - req.NetQty if req.WasteQty != expectedWasteQty { return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity for transaction %d", i) } // Set transaction date if not provided transactionDate := time.Now() if req.TransactionDate != nil { transactionDate = *req.TransactionDate } transactionEntities[i] = &entities.OrderIngredientTransaction{ ID: uuid.New(), OrganizationID: organizationID, OutletID: &outletID, OrderID: req.OrderID, OrderItemID: req.OrderItemID, ProductID: req.ProductID, ProductVariantID: req.ProductVariantID, IngredientID: req.IngredientID, GrossQty: req.GrossQty, NetQty: req.NetQty, WasteQty: req.WasteQty, Unit: req.Unit, TransactionDate: transactionDate, CreatedBy: createdBy, } } // Bulk create if err := p.orderIngredientTransactionRepo.BulkCreate(ctx, transactionEntities); err != nil { return nil, fmt.Errorf("failed to bulk create order ingredient transactions: %w", err) } // Get created entities with relations responses := make([]*models.OrderIngredientTransactionResponse, len(transactionEntities)) for i, entity := range transactionEntities { createdEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, entity.ID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get created order ingredient transaction %d: %w", i, err) } responses[i] = mappers.MapOrderIngredientTransactionEntityToResponse(createdEntity) } return responses, nil } func (p *OrderIngredientTransactionProcessorImpl) CalculateWasteQuantities(ctx context.Context, productID uuid.UUID, quantity float64, organizationID uuid.UUID) ([]*models.CreateOrderIngredientTransactionRequest, error) { // Get product ingredients productIngredients, err := p.productIngredientRepo.GetByProductID(ctx, productID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product ingredients: %w", err) } if len(productIngredients) == 0 { return []*models.CreateOrderIngredientTransactionRequest{}, nil } // Get ingredient details for unit information ingredientMap := make(map[uuid.UUID]*entities.Ingredient) for _, pi := range productIngredients { ingredient, err := p.ingredientRepo.GetByID(ctx, pi.IngredientID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get ingredient %s: %w", pi.IngredientID, err) } ingredientMap[pi.IngredientID] = ingredient } // Calculate quantities for each ingredient transactions := make([]*models.CreateOrderIngredientTransactionRequest, 0, len(productIngredients)) for _, pi := range productIngredients { ingredient := ingredientMap[pi.IngredientID] // Calculate net quantity (actual quantity needed for the product) netQty := pi.Quantity * quantity // Calculate gross quantity (including waste) wasteMultiplier := 1 + (pi.WastePercentage / 100) grossQty := netQty * wasteMultiplier // Calculate waste quantity wasteQty := grossQty - netQty // Get unit name unitName := "unit" // default if ingredient.UnitID != uuid.Nil { unit, err := p.unitRepo.GetByID(ctx, ingredient.UnitID, organizationID) if err == nil { unitName = unit.Name } } transaction := &models.CreateOrderIngredientTransactionRequest{ IngredientID: pi.IngredientID, GrossQty: util.RoundToDecimalPlaces(grossQty, 3), NetQty: util.RoundToDecimalPlaces(netQty, 3), WasteQty: util.RoundToDecimalPlaces(wasteQty, 3), Unit: unitName, } transactions = append(transactions, transaction) } return transactions, nil }