Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 1011 - new "getPriceProducts" method #1012

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/openfoodfacts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export 'src/prices/get_locations_result.dart';
// export 'src/prices/get_parameters_helper.dart'; // uncomment if really needed
export 'src/prices/get_prices_order.dart';
export 'src/prices/get_prices_parameters.dart';
export 'src/prices/get_price_products_order.dart';
export 'src/prices/get_price_products_parameters.dart';
export 'src/prices/get_price_products_result.dart';
export 'src/prices/get_prices_result.dart';
export 'src/prices/get_price_count_parameters_helper.dart';
export 'src/prices/get_proofs_order.dart';
Expand Down
30 changes: 30 additions & 0 deletions lib/src/open_prices_api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import 'prices/proof.dart';
import 'prices/get_locations_parameters.dart';
import 'prices/get_locations_result.dart';
import 'prices/get_parameters_helper.dart';
import 'prices/get_price_products_parameters.dart';
import 'prices/get_price_products_result.dart';
import 'prices/get_prices_parameters.dart';
import 'prices/get_prices_result.dart';
import 'prices/get_proofs_parameters.dart';
Expand Down Expand Up @@ -163,6 +165,34 @@ class OpenPricesAPIClient {
return MaybeError<Location>.responseError(response);
}

static Future<MaybeError<GetPriceProductsResult>> getPriceProducts(
final GetPriceProductsParameters parameters, {
final UriProductHelper uriHelper = uriHelperFoodProd,
final String? bearerToken,
}) async {
final Uri uri = getUri(
path: '/api/v1/products',
queryParameters: parameters.getQueryParameters(),
uriHelper: uriHelper,
);
final Response response = await HttpHelper().doGetRequest(
uri,
uriHelper: uriHelper,
bearerToken: bearerToken,
);
if (response.statusCode == 200) {
try {
final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response);
return MaybeError<GetPriceProductsResult>.value(
GetPriceProductsResult.fromJson(decodedResponse),
);
} catch (e) {
//
}
}
return MaybeError<GetPriceProductsResult>.responseError(response);
}

static Future<MaybeError<PriceProduct>> getPriceProductById(
final int productId, {
final UriProductHelper uriHelper = uriHelperFoodProd,
Expand Down
8 changes: 4 additions & 4 deletions lib/src/prices/get_parameters_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ abstract class GetParametersHelper<T extends OrderByField> {

/// Returns the parameters as a query parameter map.
Map<String, String> getQueryParameters() {
_checkIntValue('page_number', pageSize, min: 1);
_checkIntValue('page_number', pageNumber, min: 1);
_checkIntValue('page_size', pageSize, min: 1, max: 100);
_result.clear();
addNonNullInt(pageNumber, 'page');
Expand All @@ -32,7 +32,7 @@ abstract class GetParametersHelper<T extends OrderByField> {
}

void _checkIntValue(
final String field,
final String fieldDescription,
final int? value, {
final int? min,
final int? max,
Expand All @@ -43,14 +43,14 @@ abstract class GetParametersHelper<T extends OrderByField> {
if (min != null) {
if (value < min) {
throw Exception(
'$field minimum value is $min (actual value is $value)',
'$fieldDescription minimum value is $min (actual value is $value)',
);
}
}
if (max != null) {
if (value > max) {
throw Exception(
'$field maximum value is $max (actual value is $value)',
'$fieldDescription maximum value is $max (actual value is $value)',
);
}
}
Expand Down
13 changes: 13 additions & 0 deletions lib/src/prices/get_price_products_order.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'order_by.dart';

/// Field for the "order by" clause of "get price products".
enum GetPriceProductsOrderField implements OrderByField {
priceCount(offTag: 'price_count'),
created(offTag: 'created'),
updated(offTag: 'updated');

const GetPriceProductsOrderField({required this.offTag});

@override
final String offTag;
}
37 changes: 37 additions & 0 deletions lib/src/prices/get_price_products_parameters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:openfoodfacts/src/prices/flavor.dart';

import 'get_price_count_parameters_helper.dart';
import 'get_price_products_order.dart';

/// Parameters for the "get price products" API query.
class GetPriceProductsParameters
extends GetPriceCountParametersHelper<GetPriceProductsOrderField> {
String? brandsLike;
String? brandsTagsContains;
String? categoriesTagsContains;
String? code;
String? ecoscoreGrade;
String? labelsTagsContains;
String? novaGroup;
String? nutriscoreGrade;
String? productNameLike;
int? uniqueScansNGte;
Flavor? source;

@override
Map<String, String> getQueryParameters() {
super.getQueryParameters();
addNonNullString(brandsLike, 'brands__like');
addNonNullString(brandsTagsContains, 'brands_tags__contains');
addNonNullString(categoriesTagsContains, 'categories_tags__contains');
addNonNullString(code, 'code');
addNonNullString(ecoscoreGrade, 'ecoscore_grade');
addNonNullString(labelsTagsContains, 'labels_tags__contains');
addNonNullString(novaGroup, 'nova_group');
addNonNullString(nutriscoreGrade, 'nutriscore_grade');
addNonNullString(productNameLike, 'product_name__like');
addNonNullInt(uniqueScansNGte, 'unique_scans_n__gte');
addNonNullString(source?.offTag, 'source');
return result;
}
}
33 changes: 33 additions & 0 deletions lib/src/prices/get_price_products_result.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:openfoodfacts/openfoodfacts.dart';

import '../interface/json_object.dart';

part 'get_price_products_result.g.dart';

/// List of price product objects returned by the "get price products" method.
@JsonSerializable()
class GetPriceProductsResult extends JsonObject {
@JsonKey()
List<PriceProduct>? items;

@JsonKey()
int? total;

@JsonKey(name: 'page')
int? pageNumber;

@JsonKey(name: 'size')
int? pageSize;

@JsonKey(name: 'pages')
int? numberOfPages;

GetPriceProductsResult();

factory GetPriceProductsResult.fromJson(Map<String, dynamic> json) =>
_$GetPriceProductsResultFromJson(json);

@override
Map<String, dynamic> toJson() => _$GetPriceProductsResultToJson(this);
}
28 changes: 28 additions & 0 deletions lib/src/prices/get_price_products_result.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

140 changes: 130 additions & 10 deletions test/api_prices_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import 'test_constants.dart';

void main() {
OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT;
const String invalidBearerToken = 'invalid bearer token';
const int HTTP_OK = 200;
const int pageNumber = 1;
const int pageSize = 20;

group('$OpenPricesAPIClient default', () {
const UriProductHelper uriHelper = uriHelperFoodProd;
Expand Down Expand Up @@ -173,6 +174,7 @@ void main() {
..priceWithoutDiscount = 13
..priceIsDiscounted = true;

const String invalidBearerToken = 'invalid bearer token';
String bearerToken = invalidBearerToken;

// failing price creation with invalid token
Expand Down Expand Up @@ -372,8 +374,6 @@ void main() {

test('get prices', () async {
const UriProductHelper uriHelper = uriHelperFoodProd;
const int pageNumber = 1;
const int pageSize = 20;

late GetPricesResult result;

Expand Down Expand Up @@ -556,9 +556,6 @@ void main() {
});

test('get locations', () async {
const int pageNumber = 1;
const int pageSize = 20;

late GetLocationsResult result;

// oldest first
Expand Down Expand Up @@ -707,6 +704,133 @@ void main() {
'No Product matches the given query.',
);
});

test('get products', () async {
late GetPriceProductsResult result;

// oldest first
GetPriceProductsParameters parameters = GetPriceProductsParameters()
..orderBy = <OrderBy<GetPriceProductsOrderField>>[
OrderBy(field: GetPriceProductsOrderField.created, ascending: true),
]
..pageSize = pageSize
..pageNumber = pageNumber;
MaybeError<GetPriceProductsResult> maybeResults;
try {
maybeResults = await OpenPricesAPIClient.getPriceProducts(
parameters,
uriHelper: uriHelper,
);
} catch (e) {
if (e.toString().contains(TestConstants.badGatewayError)) {
return;
}
rethrow;
}
expect(maybeResults.isError, isFalse);
result = maybeResults.value;
expect(result.pageSize, pageSize);
expect(result.pageNumber, pageNumber);
expect(result.total, isNotNull);
expect(result.numberOfPages, (result.total! / result.pageSize!).ceil());
expect(result.items, isNotNull);
expect(result.items, hasLength(pageSize));
final DateTime oldestDate = result.items!.first.created;

// newest first
parameters = GetPriceProductsParameters()
..orderBy = <OrderBy<GetPriceProductsOrderField>>[
OrderBy(field: GetPriceProductsOrderField.created, ascending: false),
]
..pageSize = pageSize
..pageNumber = pageNumber;
try {
maybeResults = await OpenPricesAPIClient.getPriceProducts(
parameters,
uriHelper: uriHelper,
);
} catch (e) {
if (e.toString().contains(TestConstants.badGatewayError)) {
return;
}
rethrow;
}
expect(maybeResults.isError, isFalse);
result = maybeResults.value;
expect(result.pageSize, pageSize);
expect(result.pageNumber, pageNumber);
expect(result.total, isNotNull);
expect(result.numberOfPages, (result.total! / result.pageSize!).ceil());
expect(result.items, isNotNull);
expect(result.items, hasLength(pageSize));
final DateTime newestDate = result.items!.first.created;

expect(
newestDate.millisecondsSinceEpoch,
greaterThan(oldestDate.millisecondsSinceEpoch),
);

// most prices first
parameters = GetPriceProductsParameters()
..orderBy = <OrderBy<GetPriceProductsOrderField>>[
OrderBy(
field: GetPriceProductsOrderField.priceCount,
ascending: false,
),
]
..pageSize = pageSize
..pageNumber = pageNumber;
try {
maybeResults = await OpenPricesAPIClient.getPriceProducts(
parameters,
uriHelper: uriHelper,
);
} catch (e) {
if (e.toString().contains(TestConstants.badGatewayError)) {
return;
}
rethrow;
}
expect(maybeResults.isError, isFalse);
result = maybeResults.value;
expect(result.pageSize, pageSize);
expect(result.pageNumber, pageNumber);
expect(result.total, isNotNull);
expect(result.numberOfPages, (result.total! / result.pageSize!).ceil());
expect(result.items, isNotNull);
expect(result.items, hasLength(pageSize));
// value as of 2024-12-05
expect(result.items!.first.priceCount, greaterThanOrEqualTo(107));

parameters = GetPriceProductsParameters()..brandsLike = 'ferrero';
maybeResults = await OpenPricesAPIClient.getPriceProducts(
parameters,
uriHelper: uriHelper,
);
expect(maybeResults.isError, isFalse);
result = maybeResults.value;
// value as of 2024-12-05
expect(result.total, greaterThanOrEqualTo(2040));

// values as of 2024-12-05
const Map<Flavor?, int> expectedMinCounts = <Flavor?, int>{
Flavor.openFoodFacts: 3625952,
Flavor.openBeautyFacts: 31463,
Flavor.openPetFoodFacts: 9955,
Flavor.openProductFacts: 15741,
null: 3688608,
};
for (final Flavor? flavor in expectedMinCounts.keys) {
parameters = GetPriceProductsParameters()..source = flavor;
maybeResults = await OpenPricesAPIClient.getPriceProducts(
parameters,
uriHelper: uriHelper,
);
expect(maybeResults.isError, isFalse);
result = maybeResults.value;
expect(result.total, greaterThanOrEqualTo(expectedMinCounts[flavor]!));
}
});
});

group('$OpenPricesAPIClient Proofs', () {
Expand All @@ -729,8 +853,6 @@ void main() {
});

test('get proofs', () async {
const int pageNumber = 1;
const int pageSize = 20;
const GetProofsOrderField orderField = GetProofsOrderField.created;
const ProofType proofType = ProofType.receipt;

Expand Down Expand Up @@ -857,8 +979,6 @@ void main() {
const UriProductHelper uriHelper = uriHelperFoodProd;

test('get users', () async {
const int pageNumber = 1;
const int pageSize = 20;
const GetUsersOrderField orderField = GetUsersOrderField.priceCount;

late GetUsersResult result;
Expand Down
Loading