apskel-pos-backend/internal/processor/order_ingredient_transaction_processor.go
Aditya Siregar 4f6208e479 fix
2025-09-13 02:17:51 +07:00

394 lines
15 KiB
Go

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
productRecipeRepo ProductRecipeRepository
ingredientRepo IngredientRepository
unitRepo UnitRepository
}
func NewOrderIngredientTransactionProcessorImpl(
orderIngredientTransactionRepo OrderIngredientTransactionRepository,
productRecipeRepo ProductRecipeRepository,
ingredientRepo IngredientRepository,
unitRepo UnitRepository,
) OrderIngredientTransactionProcessor {
return &OrderIngredientTransactionProcessorImpl{
orderIngredientTransactionRepo: orderIngredientTransactionRepo,
productRecipeRepo: productRecipeRepo,
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 recipes
productRecipes, err := p.productRecipeRepo.GetByProductID(ctx, productID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get product recipes: %w", err)
}
if len(productRecipes) == 0 {
return []*models.CreateOrderIngredientTransactionRequest{}, nil
}
// Get ingredient details for unit information
ingredientMap := make(map[uuid.UUID]*entities.Ingredient)
for _, pr := range productRecipes {
ingredient, err := p.ingredientRepo.GetByID(ctx, pr.IngredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get ingredient %s: %w", pr.IngredientID, err)
}
ingredientMap[pr.IngredientID] = ingredient
}
// Calculate quantities for each ingredient
transactions := make([]*models.CreateOrderIngredientTransactionRequest, 0, len(productRecipes))
for _, pr := range productRecipes {
ingredient := ingredientMap[pr.IngredientID]
// Calculate net quantity (actual quantity needed for the product)
netQty := pr.Quantity * quantity
// Calculate gross quantity (including waste)
wasteMultiplier := 1 + (pr.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: pr.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
}