package service import ( "fmt" "time" "github.com/gin-gonic/gin" domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement" domain "github.com/haydenhargreaves/Potion/internal/domain/recipe" domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" ) // RecipeService implements the domain.RecipeService defined in the domain module. type RecipeService struct { recipeRepository domain.RecipeRepository engagementRepository domainEngagement.EngagementRepository } // Compile-time check to ensure the RecipeService implements domain.RecipeService var _ domain.RecipeService = (*RecipeService)(nil) // NewRecipeService creates a user service object which can be passed into the context. The service // requires a recipe repository which it will use to hit the database when needed. func NewRecipeService(recipeRepository domain.RecipeRepository, engagementRepository domainEngagement.EngagementRepository) domain.RecipeService { return &RecipeService{ recipeRepository: recipeRepository, engagementRepository: engagementRepository, } } // CreateRecipe creates a recipe in the database using the recipe repository. This function requires // all the data to be present, though validation does not occur in this function. However, the UI // will enforce validation, as will the database. Errors will be returned to the called when they // occur. // // TODO: Implement validation in the API. // TODO: Implement image creation. func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) { // Ensure user is logged in if !domainServer.IsLoggedIn(ctx) { return nil, fmt.Errorf("User is not logged in.") } userId := ctx.MustGet("userId").(int) var req domain.CreateRecipeRequest if err := ctx.ShouldBindJSON(&req); err != nil { return nil, err } recipe := domain.Recipe{ Title: req.Title, Description: req.Description, Instructions: req.Instructions, Serves: req.Serves, Difficulty: req.Difficulty, Duration: req.Duration, Category: req.Category, Ingredients: req.Ingredients, Sections: req.Sections, UserId: userId, Created: time.Now(), } if err := s.recipeRepository.CreateRecipe(&recipe); err != nil { return &recipe, err } // TODO: Upload the image // if req.image != nil { // } // Create the tags if len(req.Tags) > 0 { if err := s.recipeRepository.CreateRecipeTags(recipe, req.Tags); err != nil { return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error()) } } return &recipe, nil // title := ctx.PostForm("title") // description := ctx.PostForm("description") // preparation := ctx.PostForm("preparation-time") // cook := ctx.PostForm("cook-time") // serving := ctx.PostForm("serving-size") // category := ctx.PostForm("category") // difficulty := ctx.PostForm("difficulty") // ingredients := ctx.PostFormArray("ingredients") // quantity := ctx.PostFormArray("quantity") // instructions := ctx.PostFormArray("instructions") // tags := strings.Split(ctx.PostForm("tags"), ",") // userId := ctx.MustGet("userId").(int) // // // Have to get the image differently // image, err := ctx.FormFile("image") // if err != nil && !errors.Is(err, http.ErrMissingFile) { // // Error getting image // } // // // Convert to proper values // servingInt, _ := strconv.Atoi(serving) // difficultyInt, _ := strconv.Atoi(difficulty) // prepInt, _ := strconv.Atoi(preparation) // cookInt, _ := strconv.Atoi(cook) // // var ingredientSlice []domain.RecipeIngredient // for i := range len(ingredients) { // if strings.TrimSpace(ingredients[i]) != "" { // ins := domain.RecipeIngredient{ // Name: ingredients[i], // Quantity: quantity[i], // } // // ingredientSlice = append(ingredientSlice, ins) // } // } // // var instructionSlice []string // for _, ins := range instructions { // if ins != "" { // instructionSlice = append(instructionSlice, ins) // } // } // // // Create the recipe // recipe := domain.Recipe{ // Title: title, // Description: description, // Instructions: instructionSlice, // Serves: servingInt, // Difficulty: difficultyInt, // Duration: domain.RecipeDuration{ // Total: prepInt + cookInt, // Prep: prepInt, // Cook: cookInt, // }, // Category: domain.RecipeMeal(category), // Ingredients: ingredientSlice, // UserId: userId, // Created: time.Now(), // } // // if err := s.recipeRepository.CreateRecipe(&recipe); err != nil { // return &recipe, err // } // // // TODO: Upload the image // if image != nil { // } // // // Create the tags // if len(tags) > 0 { // if err := s.recipeRepository.CreateRecipeTags(recipe, tags); err != nil { // return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error()) // } // } // // return &recipe, nil } // GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore, // if the recipe is nil, an error will be returned, so the caller does not need to check for a nil // recipe (e.g., if the error is nil the recipe exists) // // A userId should be provided to allow the favorite status to be updated. Without a userId (nil), // the favorite status will return false, not because its not a favorite, but because it cannot find // out! func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) { recipe, err := s.recipeRepository.GetRecipe(id, userId) if recipe == nil && err == nil { return nil, fmt.Errorf("Recipe does not exist or has been relocated. Please try again.") } return recipe, err } // SearchRecipes will search the database using the filters provided. The recipes can be passed into // a template and displayed in the UI as the search result. A more detailed definition of the // filters is provided below. // // Each input is given a bit value (e.g., 00001 for 1) and will be passed // back to this handler as an array. The values are then added together // and will result in a integer which represents bit values. These bits // can then be passed to the repository and are then parsed to determine // which filters should be applied. // Parsing these is simple, for each filter option, use the bitwise and (&) // operator with the value we expect for the filter. When 1, we can ensure // the filter is provided. // A function `isBitActive` in the recipe repository provides an example of // testing of testing the filter parsing. // // The favorites parameter is used to only return filters favorited by the userId provided. func (s *RecipeService) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) { ids, err := s.recipeRepository.SearchRecipes(filters, userId, favorites) if err != nil { return []domain.Recipe{}, err } return s.recipeRepository.GetRecipes(ids, userId) } // GetUserRecipes returns a list of the recipes that the user has created. The user's // ID should be provided. Any errors will be bubbled to the caller. func (s *RecipeService) GetUserRecipes(userId int) ([]domain.Recipe, error) { ids, err := s.recipeRepository.GetUserRecipesIds(userId) if err != nil { return []domain.Recipe{}, err } return s.recipeRepository.GetRecipes(ids, &userId) } // GetUserFavoriteRecipes returns a list of the recipes that the user has marked as a // favorite. The user's ID should be provided. Any errors will be bubbled to the caller. func (s *RecipeService) GetUserFavoriteRecipes(userId int) ([]domain.Recipe, error) { ids, err := s.recipeRepository.GetUserFavoriteRecipesIds(userId) if err != nil { return []domain.Recipe{}, err } return s.recipeRepository.GetRecipes(ids, &userId) } // GetUserViewedRecipes returns a list of the most recent x (limit) recipes viewed by a user, from // the provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to // the caller. func (s *RecipeService) GetUserViewedRecipes(userId, limit int) ([]domain.Recipe, error) { engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementViewed) if err != nil { return nil, err } ids := make([]int, len(engagement)) for i, eng := range engagement { ids[i] = eng.Entity } return s.recipeRepository.GetRecipes(ids, &userId) } // GetUserMadeRecipes returns a list of the most recent x (limit) recipes made by a user, from the // provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to the // caller. func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, error) { engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementMade) if err != nil { return nil, err } ids := make([]int, len(engagement)) for i, eng := range engagement { ids[i] = eng.Entity } return s.recipeRepository.GetRecipes(ids, &userId) } // GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value, // the recipe will be nil. Any errors will be bubbled to the caller. func (s *RecipeService) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) { id, err := s.recipeRepository.GetRecipeOfTheWeekId(userId) if err != nil { return nil, err } if id == nil { return nil, fmt.Errorf("[ERROR] Recipe of the week ID could not be found. It may not exist.") } return s.recipeRepository.GetRecipe(*id, userId) }