package product import ( "context" "encoding/json" "fmt" "net/http" ) // OpenFoodFacts is the client for the Open Food Facts public API. type OpenFoodFacts struct { httpClient *http.Client } // NewOpenFoodFacts creates a new OpenFoodFacts client with the default HTTP client. func NewOpenFoodFacts() *OpenFoodFacts { return &OpenFoodFacts{httpClient: &http.Client{}} } // offProduct is the JSON shape returned by the Open Food Facts v2 API. type offProduct struct { ProductName string `json:"product_name"` Brands string `json:"brands"` Nutriments offNutriments `json:"nutriments"` } type offNutriments struct { EnergyKcal100g *float64 `json:"energy-kcal_100g"` Proteins100g *float64 `json:"proteins_100g"` Fat100g *float64 `json:"fat_100g"` Carbohydrates100g *float64 `json:"carbohydrates_100g"` Fiber100g *float64 `json:"fiber_100g"` } type offResponse struct { Status int `json:"status"` Product offProduct `json:"product"` } // Fetch retrieves a product from Open Food Facts by barcode. // Returns an error if the product is not found or the API call fails. func (client *OpenFoodFacts) Fetch(requestContext context.Context, barcode string) (*Product, error) { url := fmt.Sprintf("https://world.openfoodfacts.org/api/v2/product/%s.json", barcode) httpRequest, requestError := http.NewRequestWithContext(requestContext, http.MethodGet, url, nil) if requestError != nil { return nil, fmt.Errorf("build open food facts request: %w", requestError) } httpRequest.Header.Set("User-Agent", "FoodAI/1.0") httpResponse, fetchError := client.httpClient.Do(httpRequest) if fetchError != nil { return nil, fmt.Errorf("open food facts request: %w", fetchError) } defer httpResponse.Body.Close() if httpResponse.StatusCode != http.StatusOK { return nil, fmt.Errorf("open food facts returned status %d for barcode %s", httpResponse.StatusCode, barcode) } var offResp offResponse if decodeError := json.NewDecoder(httpResponse.Body).Decode(&offResp); decodeError != nil { return nil, fmt.Errorf("decode open food facts response: %w", decodeError) } if offResp.Status == 0 { return nil, fmt.Errorf("product %s not found in open food facts", barcode) } canonicalName := offResp.Product.ProductName if canonicalName == "" { canonicalName = barcode // Fall back to barcode as canonical name } barcodeValue := barcode catalogProduct := &Product{ CanonicalName: canonicalName, Barcode: &barcodeValue, CaloriesPer100g: offResp.Product.Nutriments.EnergyKcal100g, ProteinPer100g: offResp.Product.Nutriments.Proteins100g, FatPer100g: offResp.Product.Nutriments.Fat100g, CarbsPer100g: offResp.Product.Nutriments.Carbohydrates100g, FiberPer100g: offResp.Product.Nutriments.Fiber100g, } return catalogProduct, nil }