package processor import ( "apskel-pos-be/internal/constants" "context" "fmt" "time" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" "github.com/google/uuid" ) type ProductRecipeProcessor interface { Create(ctx context.Context, req *models.CreateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) Update(ctx context.Context, id uuid.UUID, req *models.UpdateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error BulkCreate(ctx context.Context, recipes []models.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) } type ProductRecipeProcessorImpl struct { productRecipeRepo *repository.ProductRecipeRepository productRepo ProductRepository ingredientRepo IngredientRepository } func NewProductRecipeProcessor(productRecipeRepo *repository.ProductRecipeRepository, productRepo ProductRepository, ingredientRepo IngredientRepository) *ProductRecipeProcessorImpl { return &ProductRecipeProcessorImpl{ productRecipeRepo: productRecipeRepo, productRepo: productRepo, ingredientRepo: ingredientRepo, } } func (p *ProductRecipeProcessorImpl) Create(ctx context.Context, req *models.CreateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) { _, err := p.productRepo.GetByID(ctx, req.ProductID) if err != nil { return nil, fmt.Errorf("invalid product: %w", err) } _, err = p.ingredientRepo.GetByID(ctx, req.IngredientID, organizationID) if err != nil { return nil, fmt.Errorf("invalid ingredient: %w", err) } entity := &entities.ProductRecipe{ ID: uuid.New(), OrganizationID: organizationID, OutletID: req.OutletID, ProductID: req.ProductID, VariantID: req.VariantID, IngredientID: req.IngredientID, Quantity: req.Quantity, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } if err := p.productRecipeRepo.Create(ctx, entity); err != nil { return nil, fmt.Errorf("failed to create product recipe: %w", err) } createdEntity, err := p.productRecipeRepo.GetByID(ctx, entity.ID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get created product recipe: %w", err) } return p.entityToResponse(createdEntity), nil } func (p *ProductRecipeProcessorImpl) GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) { entity, err := p.productRecipeRepo.GetByID(ctx, id, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipe: %w", err) } return p.entityToResponse(entity), nil } func (p *ProductRecipeProcessorImpl) GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { entities, err := p.productRecipeRepo.GetByProductID(ctx, productID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipes by product ID: %w", err) } responses := make([]*models.ProductRecipeResponse, len(entities)) for i, entity := range entities { responses[i] = p.entityToResponse(entity) } return responses, nil } func (p *ProductRecipeProcessorImpl) GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { entities, err := p.productRecipeRepo.GetByProductAndVariantID(ctx, productID, variantID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipes by product and variant ID: %w", err) } responses := make([]*models.ProductRecipeResponse, len(entities)) for i, entity := range entities { responses[i] = p.entityToResponse(entity) } return responses, nil } func (p *ProductRecipeProcessorImpl) GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { entities, err := p.productRecipeRepo.GetByIngredientID(ctx, ingredientID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipes by ingredient ID: %w", err) } responses := make([]*models.ProductRecipeResponse, len(entities)) for i, entity := range entities { responses[i] = p.entityToResponse(entity) } return responses, nil } func (p *ProductRecipeProcessorImpl) Update(ctx context.Context, id uuid.UUID, req *models.UpdateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) { // Get existing entity existingEntity, err := p.productRecipeRepo.GetByID(ctx, id, organizationID) if err != nil { return nil, fmt.Errorf("product recipe not found: %w", err) } // Update fields existingEntity.OutletID = req.OutletID existingEntity.VariantID = req.VariantID existingEntity.Quantity = req.Quantity existingEntity.UpdatedAt = time.Now().UTC() if err := p.productRecipeRepo.Update(ctx, existingEntity); err != nil { return nil, fmt.Errorf("failed to update product recipe: %w", err) } // Get the updated entity with relations updatedEntity, err := p.productRecipeRepo.GetByID(ctx, id, organizationID) if err != nil { return nil, fmt.Errorf("failed to get updated product recipe: %w", err) } return p.entityToResponse(updatedEntity), nil } func (p *ProductRecipeProcessorImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error { if err := p.productRecipeRepo.Delete(ctx, id, organizationID); err != nil { return fmt.Errorf("failed to delete product recipe: %w", err) } return nil } func (p *ProductRecipeProcessorImpl) BulkCreate(ctx context.Context, recipes []models.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { responses := make([]*models.ProductRecipeResponse, 0, len(recipes)) for _, recipe := range recipes { response, err := p.Create(ctx, &recipe, organizationID) if err != nil { return nil, fmt.Errorf("failed to create recipe for product %s and ingredient %s: %w", recipe.ProductID, recipe.IngredientID, err) } responses = append(responses, response) } return responses, nil } func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRecipe) *models.ProductRecipeResponse { response := &models.ProductRecipeResponse{ ID: entity.ID, OrganizationID: entity.OrganizationID, OutletID: entity.OutletID, ProductID: entity.ProductID, VariantID: entity.VariantID, IngredientID: entity.IngredientID, Quantity: entity.Quantity, CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt, } if entity.Product != nil { response.Product = &models.Product{ ID: entity.Product.ID, OrganizationID: entity.Product.OrganizationID, CategoryID: entity.Product.CategoryID, SKU: entity.Product.SKU, Name: entity.Product.Name, Description: entity.Product.Description, Price: entity.Product.Price, Cost: entity.Product.Cost, BusinessType: constants.BusinessType(entity.Product.BusinessType), ImageURL: entity.Product.ImageURL, PrinterType: entity.Product.PrinterType, UnitID: entity.Product.UnitID, HasIngredients: entity.Product.HasIngredients, Metadata: entity.Product.Metadata, IsActive: entity.Product.IsActive, CreatedAt: entity.Product.CreatedAt, UpdatedAt: entity.Product.UpdatedAt, } } if entity.ProductVariant != nil { response.ProductVariant = &models.ProductVariant{ ID: entity.ProductVariant.ID, ProductID: entity.ProductVariant.ProductID, Name: entity.ProductVariant.Name, PriceModifier: entity.ProductVariant.PriceModifier, Cost: entity.ProductVariant.Cost, Metadata: entity.ProductVariant.Metadata, CreatedAt: entity.ProductVariant.CreatedAt, UpdatedAt: entity.ProductVariant.UpdatedAt, } } if entity.Ingredient != nil { response.Ingredient = &models.Ingredient{ ID: entity.Ingredient.ID, OrganizationID: entity.Ingredient.OrganizationID, OutletID: entity.Ingredient.OutletID, Name: entity.Ingredient.Name, UnitID: entity.Ingredient.UnitID, Cost: entity.Ingredient.Cost, Stock: entity.Ingredient.Stock, IsSemiFinished: entity.Ingredient.IsSemiFinished, IsActive: entity.Ingredient.IsActive, Metadata: entity.Ingredient.Metadata, CreatedAt: entity.Ingredient.CreatedAt, UpdatedAt: entity.Ingredient.UpdatedAt, } } return response }