diff --git a/lib/model/ProductFreshness.dart b/lib/model/ProductFreshness.dart new file mode 100644 index 0000000000..edf239c787 --- /dev/null +++ b/lib/model/ProductFreshness.dart @@ -0,0 +1,41 @@ +import 'package:openfoodfacts/model/Product.dart'; + +/// Freshness of a [Product]. +class ProductFreshness { + ProductFreshness._({ + required this.isEcoscoreReady, + required this.isNutriscoreReady, + required this.isIngredientsReady, + required this.lastModified, + required this.improvements, + }); + + final bool isEcoscoreReady; + final bool isNutriscoreReady; + final bool isIngredientsReady; + final DateTime? lastModified; + final Set improvements; + + factory ProductFreshness.fromProduct(final Product product) => + ProductFreshness._( + isEcoscoreReady: product.ecoscoreScore != null, + isNutriscoreReady: product.nutriscore != null, + isIngredientsReady: product.ingredientsTags != null && + product.ingredientsTags!.isNotEmpty, + lastModified: product.lastModified, + improvements: product.getProductImprovements(), + ); + + @override + String toString() => 'ProductFreshness(' + 'ecoscore:$isEcoscoreReady' + ',' + 'nutriscore:$isNutriscoreReady' + ',' + 'ingredients:$isIngredientsReady' + ',' + 'lastModified:$lastModified' + ',' + 'improvements:$improvements' + ')'; +} diff --git a/lib/openfoodfacts.dart b/lib/openfoodfacts.dart index 0019e35d21..d06d53584a 100644 --- a/lib/openfoodfacts.dart +++ b/lib/openfoodfacts.dart @@ -9,6 +9,7 @@ import 'package:openfoodfacts/interface/JsonObject.dart'; import 'package:openfoodfacts/model/KnowledgePanels.dart'; import 'package:openfoodfacts/model/OcrIngredientsResult.dart'; import 'package:openfoodfacts/model/OrderedNutrients.dart'; +import 'package:openfoodfacts/model/ProductFreshness.dart'; import 'package:openfoodfacts/model/ProductImage.dart'; import 'package:openfoodfacts/model/TaxonomyAdditive.dart'; import 'package:openfoodfacts/model/TaxonomyAllergen.dart'; @@ -23,6 +24,7 @@ import 'package:openfoodfacts/utils/ImageHelper.dart'; import 'package:openfoodfacts/utils/OcrField.dart'; import 'package:openfoodfacts/utils/PnnsGroupQueryConfiguration.dart'; import 'package:openfoodfacts/utils/PnnsGroups.dart'; +import 'package:openfoodfacts/utils/ProductFields.dart'; import 'package:openfoodfacts/utils/ProductListQueryConfiguration.dart'; import 'package:openfoodfacts/utils/QueryType.dart'; import 'package:openfoodfacts/utils/TagType.dart'; @@ -51,6 +53,7 @@ export 'model/Ingredient.dart'; export 'model/Insight.dart'; export 'model/KeyStats.dart'; export 'model/Product.dart'; +export 'model/ProductFreshness.dart'; // export 'model/ProductList.dart'; // not needed export 'model/ProductImage.dart'; export 'model/ProductResult.dart'; @@ -294,6 +297,41 @@ class OpenFoodAPIClient { return result; } + /// Returns the [ProductFreshness] for all the [barcodes]. + static Future> getProductFreshness({ + required final List barcodes, + final User? user, + final OpenFoodFactsLanguage? language, + final OpenFoodFactsCountry? country, + final QueryType? queryType, + }) async { + final SearchResult searchResult = await getProductList( + user, + ProductListQueryConfiguration( + barcodes, + language: language, + country: country, + fields: [ + ProductField.BARCODE, + ProductField.ECOSCORE_SCORE, + ProductField.NUTRISCORE, + ProductField.INGREDIENTS_TAGS, + ProductField.LAST_MODIFIED, + ProductField.STATES_TAGS, + ], + ), + queryType: queryType, + ); + final Map result = {}; + if (searchResult.products == null) { + return result; + } + for (final Product product in searchResult.products!) { + result[product.barcode!] = ProductFreshness.fromProduct(product); + } + return result; + } + // TODO: deprecated from 2021-07-13 (#92); remove when old enough @Deprecated( 'Use PnnsGroup2Filter with ProductSearchQueryConfiguration instead') diff --git a/test/api_searchProducts_test.dart b/test/api_searchProducts_test.dart index f431035667..42bdce3a42 100644 --- a/test/api_searchProducts_test.dart +++ b/test/api_searchProducts_test.dart @@ -1,8 +1,10 @@ +import 'package:openfoodfacts/model/ProductFreshness.dart'; import 'package:openfoodfacts/model/parameter/PnnsGroup2Filter.dart'; import 'package:openfoodfacts/model/parameter/SearchTerms.dart'; import 'package:openfoodfacts/model/parameter/TagFilter.dart'; import 'package:openfoodfacts/model/parameter/WithoutAdditives.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:openfoodfacts/utils/CountryHelper.dart'; import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart'; import 'package:openfoodfacts/utils/PnnsGroups.dart'; import 'package:openfoodfacts/utils/ProductListQueryConfiguration.dart'; @@ -404,6 +406,48 @@ void main() { } }); + test('product freshness', () async { + const String UNKNOWN_BARCODE = '1111111111111111111111111111111'; + const List BARCODES = [ + '8024884500403', + '3263855093192', + '3045320001570', + '3021762383344', + '4008400402222', + '3330720237255', + '3608580823513', + '3700278403936', + '3302747010029', + '3608580823490', + '3250391660995', + '3760020506605', + '8722700202387', + '3330720237330', + '3535800940005', + '20000691', + '3270190127512', + UNKNOWN_BARCODE, + ]; + + final Map result = + await OpenFoodAPIClient.getProductFreshness( + barcodes: BARCODES, + user: TestConstants.PROD_USER, + language: OpenFoodFactsLanguage.FRENCH, + country: OpenFoodFactsCountry.FRANCE, + queryType: QueryType.PROD, + ); + + int count = 0; + for (final MapEntry entry in result.entries) { + count++; + expect(entry.key == UNKNOWN_BARCODE, false); + expect(entry.key, isIn(BARCODES)); + expect(entry.value.lastModified, isNotNull); + } + expect(count, BARCODES.length - 1); + }); + test('multiple products and pagination', () async { const BARCODES = [ '8024884500403',